openapi: "3.1.0"
info:
  title: PreForm API
  version: 1.0.0-alpha
  termsOfService: /formlabs-api-license-agreement.html
  license:
    name: Formlabs API License Agreement
    url: https://formlabs.com/api-license-agreement/
  contact:
    name: Formlabs Support
    url: https://support.formlabs.com/
  x-logo:
    url: '/Formlabs-Logo-rgb-black.svg'
    altText: Formlabs Logo
  description: |
    The PreForm API prepares jobs for Formlabs printers and sends them to
    print. PreForm, or PreForm Server, its windowless edition, serves it over
    HTTP on your own machine.

    From a script or a pipeline, import models; orient, support and lay them
    out; check printability; and slice or print the result. It supports
    Formlabs SLA and SLS printers;
    [`/v1/print-setting/list/`](#operation/listPrintSettings) lists every
    printer and material a server supports.

    Every v1 endpoint is under `/v1/`.

    # How PreForm Server differs from a cloud API
    Most web APIs, such as the Formlabs Web API, are stateless REST services:
    each request stands alone, and resources live in the provider's
    database. PreForm Server is a desktop application with an HTTP
    interface. It holds your work in memory between requests and runs
    beside your files and printers.

    1. **State lives in the server process.** Scenes, models, operations and
       the Dashboard login live in the server's memory and vanish when it
       exits. Save work with [`/v1/scene/save/`](#operation/saveFormFile)
       and reopen it with [`/v1/scene/open/`](#operation/openFormFile).
       Create or open a scene first; endpoints that act on it take its
       `scene_id`.
    2. **Endpoints are actions, not resources.** Each route names an action,
       such as `/v1/scene/model/auto-orient/`, and takes a JSON body, so most
       are `POST`, reads included. A response reports what the action did,
       such as the ids it created or work it left unfinished, not the
       resulting state. Read state with
       [`/v1/scene/get/`](#operation/getScene) or
       [`/v1/scene/model/list/`](#operation/listModels).
    3. **Changes to a scene run in order.** Requests that change a scene run
       one at a time, in arrival order, and each sees the results of those
       before it. Reads don't wait: they return the scene as the last
       finished request left it.
    4. **It runs on your machine.** The server listens on localhost and has
       no API keys: whatever reaches the port can use it, so keep the port
       private to the machine. File paths in requests refer to the server's
       machine, or to its container when it runs in Docker. Endpoints that
       call Formlabs web services also need a Dashboard login
       ([`/v1/login/`](#operation/login)), and say so in their descriptions.

    # A typical session
    Work follows the server's lifetime. Start the server or connect to one
    that is running, create a scene, and change it one request at a time.
    Each request builds on the state the last one left.

    Save the scene to a `.form` file before the server exits; the scene goes
    with it.

    ```bash A session
    curl -X POST "http://localhost:44388/v1/scene/create/" \
      -H "Content-Type: application/json" \
      -d '{
        "machine_type": "FORM-4-0",
        "material_code": "FLGPGR05",
        "layer_thickness_mm": 0.1
      }'
    # {"scene_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", ...}

    curl -X POST "http://localhost:44388/v1/scene/model/import/" \
      -H "Content-Type: application/json" \
      -d '{
        "scene_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "file": "C:\\Models\\part.stl"
      }'

    curl -X POST "http://localhost:44388/v1/scene/model/auto-orient/" \
      -H "Content-Type: application/json" \
      -d '{
        "scene_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      }'

    # Write the scene to disk: it is lost when the server exits.
    curl -X POST "http://localhost:44388/v1/scene/save/" \
      -H "Content-Type: application/json" \
      -d '{
        "scene_ids": [
          "3fa85f64-5717-4562-b3fc-2c963f66afa6"
        ],
        "file": "C:\\Models\\part.form"
      }'
    ```

    # Asynchronous requests
    Slicing, importing a large CAD file and generating supports can take
    minutes. Endpoints that can run long accept `?async=true` and return
    `202 Accepted` at once, with an `operation_id` and an `operation_url`
    (also in the `Location` header).

    Poll the `operation_url` until `status` is `SUCCEEDED` or `FAILED`;
    `result` then holds the response the endpoint would have returned
    synchronously. Like scenes, operations do not survive a restart.

    Without `async`, a call blocks until it finishes or reaches the server's
    10-minute timeout.

    ```bash Asynchronous request
    curl -X POST "http://localhost:44388/v1/scene/model/import/?async=true" \
      -H "Content-Type: application/json" \
      -d '{
        "scene_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "file": "C:\\Models\\assembly.step"
      }'

    # Poll the operation_url until status is SUCCEEDED or FAILED.
    curl "http://localhost:44388/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
    ```

    ```json Response | 202 Accepted
    {
      "operation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "operation_url": "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "message": "Operation accepted. Poll GET /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
    }
    ```

    ```json Response | Polled: SUCCEEDED
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "status": "SUCCEEDED",
      "progress": 1.0,
      "result": {
        "new_models": [{ "model_id": "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10" }]
      }
    }
    ```

    # Errors
    The API uses conventional HTTP status codes, and every error body is an
    `error` object with a `code` and a `message`.

    **Program against `code`, never against `message`.** Codes are stable
    SCREAMING_SNAKE_CASE identifiers from the [ErrorCode](#tag/Errors)
    registry; new ones may appear without a version change, so handle codes
    you don't recognize. Messages are for people and may change in any
    release.

    Each endpoint's *Error codes* table lists the codes it returns and their
    causes.

    ```json Error response
    {
      "error": {
        "code": "INPUT_ERROR",
        "message": "Scene with given scene_id not found"
      }
    }
    ```

    <!-- panel: HTTP status codes -->
    | Status | Meaning |
    |--------|---------|
    | `200 OK` | The request succeeded. |
    | `202 Accepted` | Started with `?async=true`. Poll the returned operation. |
    | `400 Bad Request` | The request was malformed or referenced something that does not exist. |
    | `404 Not Found` | A referenced resource, such as a model or a connected printer, does not exist, or the path is not a v1 endpoint. The code is `NOT_FOUND`. |
    | `409 Conflict` | The server's current state prevents the request: a referenced resource is busy with another operation, or the request needs more scenes than the server can hold. |
    | `422 Unprocessable Entity` | The request is well-formed and names a real scene, but the scene's current configuration does not support it. |
    | `500 Internal Server Error` | Something failed inside the server that the request did not cause. |

    # Conventions
    ## Identifiers
    Scenes, models and operations have UUIDs. Responses give them bare
    (`3fa85f64-5717-4562-b3fc-2c963f66afa6`); requests also accept the
    braced form (`{3fa85f64-...}`).

    ## Enums
    Enum values are SCREAMING_SNAKE_CASE. **Request enums are closed:** a
    value outside the list fails with `400 JSON_SCHEMA_MATCH_ERROR`. Later
    releases may accept new values, which older servers reject the same way.
    **Response enums are open:** schemas that list their values under
    `x-extensible-enum`, instead of `enum`, may gain values in any release
    without a version change, so treat an unrecognized value as unknown
    instead of failing.

    ## Created models
    Requests that add models return their ids, in no particular order. When
    they derive from an existing model (copies, split bodies, a drilled
    plug), the response names it too, so you never need to diff the scene.

    ```json Enum in a response
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "status": "IN_PROGRESS",
      "progress": 0.42
    }
    ```
servers:
  - url: http://localhost:{port}
    description: >
      PreForm or PreForm Server on the local machine, on the port it was
      started with: PreForm Server's required `--port` argument, or the port
      chosen when starting the server from PreForm's File menu.
    variables:
      port:
        default: "44388"
security: []
externalDocs:
  description: Formlabs Developer Portal
  url: https://developer.formlabs.com/
tags:
  - name: Server Info
    description: Endpoint for checking that the server is up and reachable.
  - name: Authentication
    description: |
      Endpoints for checking and managing your Formlabs Dashboard account
      login state. Endpoints that call Formlabs web services need a login
      session, and say so in their descriptions.
  - name: Scenes
    description: |
      Create, load, and enumerate scenes. A scene is the top-level unit of
      work: it holds a printer/material/print-setting combination and the
      models placed within that printer's build volume.
  - name: Modifying Models
    description: >
      Endpoints that take actions on models within a scene.
  - name: Printability Checks
    description: |
      Scene printability checks. These endpoints analyze the scene and
      report problems. They never modify it.
  - name: Print Settings
    description: |
      The catalog of machine/material/layer-thickness/print-setting
      combinations Formlabs ships settings for, and validation of a
      candidate combination against that catalog.
  - name: Operations
    description: |
      Poll and manage the long-running operations started by endpoints
      called with `?async=true`. `/v1/operation/get/` returns one
      operation's status and, once finished, its result. See the
      *Asynchronous requests* section of the introduction.
  - name: Devices
    description: |
      Enumerate printers and Fleet Control printer groups PreForm knows
      about, discover new ones on the network, and upload firmware to
      them. Discovering Dashboard-registered printers and Fleet Control
      queues requires a Dashboard login (`/v1/login/`).
  - name: Errors
    description: |
      The shared error envelope and the registry of stable error codes.
      See the *Errors* section of the introduction for the contract.
paths:
  /v1/:
    get:
      operationId: getServerVersion
      summary: Get server version
      tags:
        - Server Info
      description: |
        Returns the version of the running PreFormServer instance. Does not
        require a request body or authentication. Use it to confirm the
        server is up and reachable before issuing other requests.
      responses:
        "200":
          description: The server responded successfully.
          content:
            application/json:
              schema:
                type: object
                required: [version]
                properties:
                  version:
                    type: string
                    description: The running PreFormServer's version string.
              examples:
                Default:
                  value:
                    version: "1.0.0"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/login/:
    post:
      operationId: login
      summary: Log in to a Formlabs Dashboard account
      tags:
        - Authentication
      description: |
        Authenticates with Formlabs Dashboard, either with a username and
        password or with a previously issued access token, and stores the
        resulting session for the lifetime of the PreFormServer process.
        Logging in again replaces any existing session.

        Some endpoints call Formlabs web services on your behalf and require
        this login state. Use `/v1/user/` to check whether a session is
        currently active, and `/v1/logout/` to end it.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body has neither a complete `username`/`password` pair nor an `access_token`, mixes the two shapes by including `access_token` alongside `username`/`password`, has a field with the wrong type, or contains an unrecognized field. The message names whichever problem applies. |
        | 400 | `OPERATION_FAILED` | The credentials or access token were rejected by Formlabs Dashboard, or Dashboard could not be reached. The message carries the underlying reason. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            examples:
              UsernameAndPassword:
                summary: Log in with a Dashboard username and password
                value:
                  username: "jane@example.com"
                  password: "hunter2"
              AccessToken:
                summary: Log in with an access token
                value:
                  access_token: "e0f3a4b1c2d5e6f7"
      responses:
        "200":
          description: Login succeeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
              examples:
                Default:
                  value:
                    access_token: "e0f3a4b1c2d5e6f7"
                    refresh_token: "9a8b7c6d5e4f3a2b"
        "400":
          description: The request was malformed, or Dashboard rejected the credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                BadCredentials:
                  summary: "OPERATION_FAILED: credentials or token rejected"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "The username/email and password combination you entered is incorrect. Please try again."
                IncompleteCredentials:
                  summary: "JSON_SCHEMA_MATCH_ERROR: incomplete body (e.g. username with no password)"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Login requires either 'access_token', or both 'username' and 'password'"
                MixedShape:
                  summary: "JSON_SCHEMA_MATCH_ERROR: access_token mixed with username/password"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Login accepts either 'access_token', or 'username' and 'password', not both"
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Error parsing request JSON"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/logout/:
    post:
      operationId: logout
      summary: Log out of Formlabs Dashboard account
      tags:
        - Authentication
      description: |
        Clears the current Dashboard login session, if any. Succeeds on
        both logout and the case where no one is logged in. Takes no
        request body.
      responses:
        "200":
          description: The session was cleared (or there was none to clear). The response has no body.
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/user/:
    get:
      operationId: getUserInfo
      summary: Get the logged-in Dashboard user
      tags:
        - Authentication
      description: |
        Returns account information for the currently logged-in Formlabs
        Dashboard user (see `/v1/login/`). The response is a passthrough
        of the Dashboard's own account record, so it may carry fields
        beyond the ones documented here.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 404 | `NOT_FOUND` | No Dashboard session is currently active. |
      responses:
        "200":
          description: A Dashboard session is active. Returns account information for that user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInfo'
              examples:
                Default:
                  value:
                    username: "jane@example.com"
                    email: "jane@example.com"
                    id: 123456
                    first_name: "Jane"
                    last_name: "Doe"
        "404":
          description: No Dashboard session is currently active.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                NotLoggedIn:
                  summary: "NOT_FOUND: no active session"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "No user is currently logged in"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/open/:
    post:
      operationId: openFormFile
      summary: Open a .form file
      tags:
        - Scenes
      description: |
        Load a saved PreForm project (`.form`) file. A `.form` file can hold
        more than one print job, each saved as its own scene. This
        endpoint creates a new scene for each one in the file and
        reports each: its id, its name in the file, its print
        settings, and the ids of the models loaded into it. Fetch a scene's
        full state with `/v1/scene/get/` or `/v1/scene/model/list/`.

        Models keep the poses saved in the file, snapped against the printer
        the file was saved for. `file` must be a `.form` file, matched on the
        extension regardless of case; anything else is rejected before the
        file is read. The project name saved in the file is returned as
        `project_name` (empty when the file has none).

        Loading can take a while for large or multi-scene files,
        so prefer `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `file` is missing, isn't a string, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `file`'s extension is not `.form`. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | The file could not be loaded: it does not exist, is not a valid `.form` file, or its material settings could not be loaded. The message carries the underlying reason. |
        | 409 | `BUILD_VOLUME_LIMIT_EXCEEDED` | The file holds more build volumes than the server can keep at once. No scene was created. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OpenFormRequest'
            examples:
              Default:
                value:
                  file: "C:\\Users\\user\\Desktop\\project.form"
      responses:
        "200":
          description: >
            The file was loaded. Reports one entry per scene saved in
            the file, in the order they appear in the file.
          content:
            application/json:
              schema:
                type: object
                required: [project_name, scenes]
                properties:
                  project_name:
                    type: string
                    description: >
                      The project name saved in the file; empty when the file
                      carries none. Set it when saving with `/v1/scene/save/`.
                  scenes:
                    type: array
                    items:
                      $ref: '#/components/schemas/OpenedScene'
              examples:
                OneScene:
                  summary: A .form file with a single Form 4 scene
                  value:
                    project_name: "Print Job"
                    scenes:
                      - scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                        scene_name: "Print Job 1"
                        scene_settings:
                          machine_type: "FORM-4-0"
                          material_code: "FLGPBK05"
                          print_setting: "DEFAULT"
                          layer_thickness_mm: "0.05"
                        new_model_ids:
                          - "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                TwoScenes:
                  summary: A .form file holding two print jobs
                  value:
                    project_name: "Weekly batch"
                    scenes:
                      - scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                        scene_name: "Print Job 1"
                        scene_settings:
                          machine_type: "FORM-4-0"
                          material_code: "FLGPBK05"
                          print_setting: "DEFAULT"
                          layer_thickness_mm: "0.05"
                        new_model_ids:
                          - "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                          - "0d6f1e4a-2b3c-4d5e-8f90-1a2b3c4d5e6f"
                      - scene_id: "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
                        scene_name: "Print Job 2"
                        scene_settings:
                          machine_type: "FORM-4-0"
                          material_code: "FLGPBK05"
                          print_setting: "DEFAULT"
                          layer_thickness_mm: "0.05"
                        new_model_ids: []
        "202":
          description: >
            The file was accepted for asynchronous loading (`?async=true`).
            Poll the operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, or the file could not be loaded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Invalid JSON input"
                MissingFile:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'file'."
                WrongTypeFile:
                  summary: "JSON_SCHEMA_MATCH_ERROR: file is not a string"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Value type not permitted by 'type' constraint. in reference to key path: /file"
                NotAnObject:
                  summary: "JSON_PARSE_ERROR: body parsed but is not a JSON object"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Request body must be a valid JSON object"
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
                LoadFailed:
                  summary: "OPERATION_FAILED: the file could not be loaded"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Failed loading form file."
        "409":
          description: The file holds more build volumes than the server can keep at once. No scene was created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                TooManyBuildVolumes:
                  summary: "BUILD_VOLUME_LIMIT_EXCEEDED: the file holds too many build volumes"
                  value:
                    error:
                      code: "BUILD_VOLUME_LIMIT_EXCEEDED"
                      message: "The build volume limit has been reached"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/create/:
    post:
      operationId: createScene
      summary: Create a scene
      tags:
        - Scenes
      description: |
        Create a new, empty scene for a given machine/material/print-setting/
        layer-thickness combination, or from a saved `.fps` print settings
        file. See `/v1/print-setting/list/` for the catalog of supported
        combinations.

        The response reports the new scene's id and the print settings it
        resolved to (for example, the default `print_setting` when the
        request omitted one). It does not describe the rest of the scene.
        To read the scene's full state, including its build volume
        dimensions, material usage, and layer count, call `/v1/scene/get/`
        with the returned `scene_id`. That endpoint is a `POST` that takes
        the id in a JSON body:

        ```json
        POST /v1/scene/get/
        { "scene_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }
        ```

        Unlike `/v1/scene/open/`, this always completes synchronously (an
        empty scene has nothing to load).

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body matches neither request shape: a required field is missing, a field has the wrong type, an unrecognized field is present, or `layer_thickness_mm` is a string other than `"ADAPTIVE"` or a numeric string. The message includes a JSON pointer to the offending field where applicable. |
        | 400 | `INPUT_ERROR` | `machine_type` is (or canonicalizes to) the reserved unknown id, the combination of `machine_type`/`material_code`/`print_setting`/`layer_thickness_mm` does not match any shipped print setting, or `fps_file` could not be loaded or contains no usable material setting. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSceneRequest'
            examples:
              Form4BlackV5:
                summary: Form 4, Black V5, default settings
                value:
                  machine_type: "FORM-4-0"
                  material_code: "FLGPBK05"
                  print_setting: "DEFAULT"
                  layer_thickness_mm: 0.025
              Fuse1PlusNylon11:
                summary: Fuse 1+, Nylon 11, default settings
                description: >
                  print_setting is optional. When omitted, the material's
                  default print setting is used.
                value:
                  machine_type: "FS30-1-0"
                  material_code: "FLP11B01"
                  layer_thickness_mm: "0.11"
              FuseX1Nylon12:
                summary: Fuse X1, Nylon 12, default settings
                value:
                  machine_type: "FUSX-1-0"
                  material_code: "FLP12G01"
                  print_setting: "DEFAULT"
                  layer_thickness_mm: 0.11
              FpsFile:
                summary: From a saved .fps print settings file
                description: >
                  Loads (and, if custom, persists) the print setting from
                  disk. The created scene's scene_settings.custom_print_setting_id
                  identifies it.
                value:
                  fps_file: "C:\\Users\\user\\Desktop\\custom.fps"
      responses:
        "200":
          description: The scene was created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedScene'
              examples:
                NewEmptyScene:
                  summary: A freshly created Form 4 scene
                  value:
                    scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                    scene_settings:
                      machine_type: "FORM-4-0"
                      material_code: "FLGPBK05"
                      print_setting: "DEFAULT"
                      layer_thickness_mm: "0.025"
                FromFpsFile:
                  summary: Created from a custom .fps file
                  value:
                    scene_id: "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
                    scene_settings:
                      machine_type: "FORM-4-0"
                      material_code: "FLGPBK05"
                      print_setting: "DEFAULT"
                      layer_thickness_mm: "0.05"
                      custom_print_setting_id: "e0f1a2b3-c4d5-4e6f-8a90-b1c2d3e4f5a6"
        "400":
          description: The request was malformed, or no matching scene type could be created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Error parsing request JSON"
                MissingRequiredField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'layer_thickness_mm'."
                UnknownField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: unrecognized field present"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Object contains a property that could not be validated using 'properties' or 'additionalProperties' constraints: 'bogus'."
                InvalidLayerThickness:
                  summary: "JSON_SCHEMA_MATCH_ERROR: layer_thickness_mm isn't ADAPTIVE or numeric"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Failed to match against any enum values. in reference to key path: /layer_thickness_mm"
                InvalidMachineType:
                  summary: "INPUT_ERROR: machine_type is the reserved unknown id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Invalid machine_type input"
                SceneTypeNotSupported:
                  summary: "INPUT_ERROR: combination doesn't match any shipped print setting"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene type not supported"
                FpsFileLoadFailed:
                  summary: "INPUT_ERROR: fps_file could not be loaded"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Failed to import Print Setting from 'C:\\Users\\user\\Desktop\\custom.fps': file not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/list/:
    get:
      operationId: listScenes
      summary: List scenes
      tags:
        - Scenes
      description: |
        List every scene currently held by the server, in project order.
        Project order is the order the scenes appear in the project, which
        is the order a user of the desktop application sees in the build
        list. Creating a scene appends it, so for a client that only ever
        creates scenes this is also creation order. A freshly started
        server always has at least one (default) scene, and it is first.

        ### Error codes

        This endpoint takes no input and always succeeds. Only the shared
        [default error](#tag/Errors) can occur.
      responses:
        "200":
          description: The current scenes.
          content:
            application/json:
              schema:
                type: object
                required: [scenes]
                properties:
                  scenes:
                    type: array
                    items:
                      $ref: '#/components/schemas/Scene'
              examples:
                TwoScenes:
                  summary: The default scene plus one created scene
                  value:
                    scenes:
                      - id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                        models: []
                        scene_settings:
                          machine_type: "FORM-4-0"
                          material_code: "FLGPBK05"
                          print_setting: "DEFAULT"
                          layer_thickness_mm: "0.025"
                        material_usage: {volume_ml: 0.0, unsupported_volume_ml: 0.0}
                        layer_count: 0
                        build_volume:
                          dimensions_mm: {x: 200.0, y: 125.0, z: 210.0}
                          fillet_radius_mm: 3.0
                      - id: "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
                        models: []
                        scene_settings:
                          machine_type: "PILK-1-0"
                          material_code: "FLP11B01"
                          print_setting: "DEFAULT"
                          layer_thickness_mm: "0.11"
                        material_usage:
                          total_powder_ml: 0.0
                          total_powder_kg: 0.0
                          total_sintered_powder_ml: 0.0
                          total_sintered_powder_kg: 0.0
                          mass_packing_density: 0.0
                        layer_count: 0
                        build_volume:
                          dimensions_mm: {x: 165.0, y: 165.0, z: 300.0}
                          fillet_radius_mm: 0.0
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/get/:
    post:
      operationId: getScene
      summary: Get a scene
      tags:
        - Scenes
      description: |
        Returns the full current state of one scene. This does not modify
        the scene. It is a `POST` (rather than `GET`) only because the
        request includes a JSON body to identify which scene to fetch.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `MISSING_PARAMETER` | The body has no `scene_id` field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
      requestBody:
        required: true
        description: Identifies the scene to fetch.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene to fetch.
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: The scene's current state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Scene'
              examples:
                EmptyScene:
                  summary: A freshly created, empty Form 4 scene
                  value:
                    id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                    models: []
                    scene_settings:
                      machine_type: "FORM-4-0"
                      material_code: "FLGPBK05"
                      print_setting: "DEFAULT"
                      layer_thickness_mm: "0.025"
                    material_usage: {volume_ml: 0.0, unsupported_volume_ml: 0.0}
                    layer_count: 0
                    build_volume:
                      dimensions_mm: {x: 200.0, y: 125.0, z: 210.0}
                      fillet_radius_mm: 3.0
        "400":
          description: The request was malformed, or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not a JSON object"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Request body must be a valid JSON object"
                MissingSceneId:
                  summary: "MISSING_PARAMETER: no scene_id in the body"
                  value:
                    error:
                      code: "MISSING_PARAMETER"
                      message: "scene_id is required"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/delete/:
    delete:
      operationId: deleteScene
      summary: Delete a scene
      tags:
        - Scenes
      description: |
        Removes a scene. The server always keeps one default scene: deleting
        it does not remove it, but instead resets it to a fresh, empty scene
        of its current type. Deleting any other scene removes it entirely.
        A later `/v1/scene/list/` no longer includes it. The delete waits for
        requests already running on the scene to finish.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `MISSING_PARAMETER` | The body has no `scene_id` field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
      requestBody:
        required: true
        description: Identifies the scene to delete.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene to delete.
            examples:
              Default:
                value:
                  scene_id: "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
      responses:
        "200":
          description: The scene was deleted (or reset, if it was the default scene). The response has no body.
        "400":
          description: The request was malformed, or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not a JSON object"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Request body must be a valid JSON object"
                MissingSceneId:
                  summary: "MISSING_PARAMETER: no scene_id in the body"
                  value:
                    error:
                      code: "MISSING_PARAMETER"
                      message: "scene_id is required"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/save/:
    post:
      operationId: saveFormFile
      summary: Save one or more scenes to a .form file
      tags:
        - Scenes
      description: |
        Saves one or more scenes as a `.form` project file. Saving a single
        scene id writes a single-scene `.form` file. Saving more
        than one writes a multi-scene project file with one print
        job per scene, in the given order. `project_name` names the project
        inside the file and is what `/v1/scene/open/` later returns. Each
        scene is written with the `scene_name` it had when
        `/v1/scene/open/` created it, so a file opened and saved keeps its
        names; scenes made with `/v1/scene/create/` have none. Scene
        names are limited to 128 characters and longer ones are shortened
        when saved. When a project name or a scene name is present, a
        single scene is also written as a project file so the name is kept.

        Saving can take a while for scenes with many models, so prefer
        `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `file` is missing or empty, `scene_ids` is missing, not an array, or empty, or an unrecognized field is present. The message includes a JSON pointer to the offending field where applicable. |
        | 400 | `INPUT_ERROR` | An entry in `scene_ids` is not a well-formed UUID, or does not refer to an existing scene. Entries are checked in order. The first bad entry is reported and no others are checked. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SaveSceneRequest'
            examples:
              SingleScene:
                summary: Save one scene as a single-build-volume .form file
                value:
                  file: "C:\\Users\\user\\Desktop\\project.form"
                  scene_ids: ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]
              MultipleScenes:
                summary: Save two scenes as one multi-build-volume project file
                value:
                  file: "C:\\Users\\user\\Desktop\\project.form"
                  project_name: "Tuesday batch"
                  scene_ids:
                    - "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                    - "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
      responses:
        "200":
          description: The file was saved. The response has no body.
        "202":
          description: >
            The file was accepted for asynchronous saving (`?async=true`).
            Poll the operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Invalid JSON input"
                MissingFile:
                  summary: "JSON_SCHEMA_MATCH_ERROR: file is missing"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'file'."
                EmptyFile:
                  summary: "JSON_SCHEMA_MATCH_ERROR: file is an empty string"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "String should be no fewer than 1 characters in length. in reference to key path: /file"
                MissingSceneIds:
                  summary: "JSON_SCHEMA_MATCH_ERROR: scene_ids is missing, not an array, or empty"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Array should contain no fewer than 1 elements. in reference to key path: /scene_ids"
                UnknownField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: unrecognized field present"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Object contains a property that could not be validated using 'properties' or 'additionalProperties' constraints: 'bogus'."
                InvalidSceneId:
                  summary: "INPUT_ERROR: an entry in scene_ids is not a UUID"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Invalid scene_id format in scene_ids"
                SceneNotFound:
                  summary: "INPUT_ERROR: an entry in scene_ids does not exist"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/render/:
    post:
      operationId: renderScene
      summary: Render a scene to an image file
      tags:
        - Scenes
      description: |
        Renders the scene's 3D view to a `.png` file or a `.webp` flyaround
        animation. This does not modify the scene.

        `view_type` selects the camera framing. `ZOOM_ON_MODELS` fits the
        visible models, `FULL_BUILD_VOLUME` shows the whole build volume,
        `FULL_PLATFORM_WIDTH` shows the full platform width at whatever
        height that implies. `model_ids` restricts which models are visible
        in the render and are candidates for `ZOOM_ON_MODELS` framing.
        Omitting this field means every model in the scene is included.

        A flyaround is a multi-frame `.webp` animation orbiting the scene at
        a fixed pitch: either an explicit list of `yaw`/`pitch` keyframes, or
        an evenly-spaced `frame_count` of frames around a full turn starting
        from `yaw`/`pitch`. Flyarounds cannot be saved as `.png`.

        Rendering can take a while for large scenes, so prefer `?async=true` and
        poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body fails the request schema: `scene_id` or `file` missing, a field has the wrong type or an out-of-range value (including a `view_type` outside the documented values and a `frame_count` below 1), `flyaround_transform` has neither `transform` nor `frame_count`, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, no scene with that id exists, or `file` ends in `.png` while `flyaround_transform` is present. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | `view_type` is `ZOOM_ON_MODELS` and no selected models are visible, or the render could not be written to `file`. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RenderRequest'
            examples:
              SingleRender:
                summary: A single .png render of two models
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  file: "C:\\Users\\user\\Desktop\\preview.png"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10", "389bef9a-fb24-6a18-4cd1-5c634e76db34"]
                  view_type: "ZOOM_ON_MODELS"
                  image_size_px: 1024
              Flyaround:
                summary: An 8-frame .webp flyaround of the whole scene
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  file: "C:\\Users\\user\\Desktop\\flyaround.webp"
                  view_type: "FULL_BUILD_VOLUME"
                  flyaround_transform:
                    frame_count: 8
              FlyaroundWithYawPitch:
                summary: An 8-frame .webp flyaround with yaw/pitch keyframes
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  file: "C:\\Users\\user\\Desktop\\flyaround.webp"
                  view_type: "FULL_BUILD_VOLUME"
                  flyaround_transform:
                    transform: [{"yaw": 0, "pitch": 45}, {"yaw": 45, "pitch": 45}, {"yaw": 90, "pitch": 45}, {"yaw": 135, "pitch": 45}, {"yaw": 180, "pitch": 45}, {"yaw": 225, "pitch": 45}, {"yaw": 270, "pitch": 45}, {"yaw": 315, "pitch": 45}]
      responses:
        "200":
          description: The image was saved. The response has no body.
        "202":
          description: >
            The render was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the returned
            id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, or the render could not be produced.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                InvalidViewType:
                  summary: "JSON_SCHEMA_MATCH_ERROR: view_type is not a recognized value"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Failed to match against any enum values. in reference to key path: /view_type"
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                UnknownField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: unrecognized field present"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Object contains a property that could not be validated using 'properties' or 'additionalProperties' constraints: 'bogus'."
                FlyaroundAsPng:
                  summary: "INPUT_ERROR: flyaround requested with a .png file"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Cannot save flyaround as PNG"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
                NoModelsVisible:
                  summary: "OPERATION_FAILED: no models to photograph"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "No models found"
        "404":
          description: >
            A model in `model_ids` does not exist in the scene. Nothing was
            rendered or written.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/estimate-print-time/:
    post:
      operationId: estimatePrintTime
      summary: Estimate a scene's print time
      tags:
        - Scenes
      description: |
        Returns a fast, approximate total print time for a scene. For a more
        accurate calculation and breakdown, use
        `/v1/scene/calculate-print-time/`. This does not modify the scene.

        Fast estimation is only available for SLA-type scenes (e.g. Form 4).
        SLS-type scenes (e.g. Fuse 1+, Fuse X1) do not support it and return `422`.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `MISSING_PARAMETER` | The body has no `scene_id` field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | The estimation task could not run (e.g. the scene was deleted while the request was in flight). |
        | 422 | `ESTIMATION_NOT_SUPPORTED` | The scene's machine type does not support fast print time estimation. |
      requestBody:
        required: true
        description: Identifies the scene to estimate.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene to estimate.
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: The estimated total print time.
          content:
            application/json:
              schema:
                type: object
                required: [total_print_time_s]
                properties:
                  total_print_time_s:
                    type: integer
                    description: Estimated total print time, in seconds.
              examples:
                Estimate:
                  value:
                    total_print_time_s: 1800
        "400":
          description: The request was malformed, or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not a JSON object"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Request body must be a valid JSON object"
                MissingSceneId:
                  summary: "MISSING_PARAMETER: no scene_id in the body"
                  value:
                    error:
                      code: "MISSING_PARAMETER"
                      message: "scene_id is required"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        "422":
          description: The scene's machine type does not support fast print time estimation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                NotSupported:
                  summary: "ESTIMATION_NOT_SUPPORTED: machine type has no fast estimation"
                  value:
                    error:
                      code: "ESTIMATION_NOT_SUPPORTED"
                      message: "Fast print time estimation is not supported for this machine type or configuration."
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/calculate-print-time/:
    post:
      operationId: calculatePrintTime
      summary: Calculate a scene's full print time breakdown
      tags:
        - Scenes
      description: |
        Runs the full print time calculation for a scene and returns a
        breakdown of total, pre-print, and printing time. This does not
        modify the scene.

        This calculation is available for every supported machine type, but
        can take noticeably longer to run than `/v1/scene/estimate-print-time/`,
        so prefer `?async=true` and poll the returned operation.

        `cool_to_removal_time_s` and `additional_cool_to_room_temp_time_s`
        are only present for Fuse 1+ scenes.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `MISSING_PARAMETER` | The body has no `scene_id` field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | The print time calculation could not complete for this scene. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        description: Identifies the scene to calculate print time for.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene to calculate print time for.
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: The full print time breakdown.
          content:
            application/json:
              schema:
                type: object
                required: [total_print_time_s, preprint_time_s, printing_time_s]
                properties:
                  total_print_time_s:
                    type: integer
                    description: Total print time in seconds, including pre-print time and, for Fuse 1+ scenes, cooling time.
                  preprint_time_s:
                    type: integer
                    description: Time spent on pre-print steps (e.g. heating the tank), in seconds.
                  printing_time_s:
                    type: integer
                    description: Time spent actively printing, in seconds.
                  cool_to_removal_time_s:
                    type: integer
                    description: Fuse 1+ scenes only. Time to cool the print to a safe removal temperature, in seconds.
                  additional_cool_to_room_temp_time_s:
                    type: integer
                    description: Fuse 1+ scenes only. Additional time to cool the print to room temperature after removal, in seconds.
              examples:
                SLAScene:
                  summary: A Form 4 (SLA-type) scene
                  value:
                    total_print_time_s: 420
                    preprint_time_s: 120
                    printing_time_s: 300
                SLSScene:
                  summary: A Fuse 1+ (SLS-type) scene
                  value:
                    total_print_time_s: 480
                    preprint_time_s: 120
                    printing_time_s: 300
                    cool_to_removal_time_s: 60
                    additional_cool_to_room_temp_time_s: 600
        "202":
          description: >
            The calculation was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the returned
            id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the calculation could not complete.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not a JSON object"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Request body must be a valid JSON object"
                MissingSceneId:
                  summary: "MISSING_PARAMETER: no scene_id in the body"
                  value:
                    error:
                      code: "MISSING_PARAMETER"
                      message: "scene_id is required"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
                CalculationFailed:
                  summary: "OPERATION_FAILED: the calculation could not complete"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Unsupported machine type."
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/update/:
    post:
      operationId: updateScene
      summary: Update a scene's printer/material settings
      tags:
        - Scenes
      description: |
        Change an existing scene's machine/material/print-setting/
        layer-thickness combination, or apply a saved `.fps` print settings
        file. Accepts the same combinations as `/v1/scene/create/`, plus the
        `scene_id` to update.

        Models already in the scene are kept and repositioned to fit the new
        machine/material's build volume. Returns the settings that were
        applied. Fetch `/v1/scene/get/` for the scene's resulting state.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body matches neither request shape: a required field (including `scene_id`) is missing, a field has the wrong type, an unrecognized field is present, or `layer_thickness_mm` is a string other than `"ADAPTIVE"` or a numeric string. The message includes a JSON pointer to the offending field where applicable. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, no scene with that id exists, `machine_type` is the reserved unknown id, the combination of `machine_type`/`material_code`/`print_setting`/`layer_thickness_mm` does not match any shipped print setting, or `fps_file` could not be loaded. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSceneRequest'
            examples:
              Form4BlackV5:
                summary: Switch a scene to Form 4, Black V5, default settings
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  machine_type: "FORM-4-0"
                  material_code: "FLGPBK05"
                  print_setting: "DEFAULT"
                  layer_thickness_mm: 0.025
              FpsFile:
                summary: From a saved .fps print settings file
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  fps_file: "C:\\Users\\user\\Desktop\\custom.fps"
      responses:
        "200":
          description: >
            The scene was updated. Returns the settings that were applied,
            including the print setting and layer thickness they resolved to.
          content:
            application/json:
              schema:
                type: object
                required: [scene_settings]
                properties:
                  scene_settings:
                    $ref: '#/components/schemas/SceneType'
              examples:
                UpdatedScene:
                  summary: A scene switched to a new material
                  value:
                    scene_settings:
                      machine_type: "FORM-4-0"
                      material_code: "FLGPBK05"
                      print_setting: "DEFAULT"
                      layer_thickness_mm: "0.025"
        "400":
          description: The request was malformed, or referenced a missing scene or unsupported scene type.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not valid JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Error parsing request JSON"
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                MissingRequiredField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'layer_thickness_mm'."
                SceneTypeNotSupported:
                  summary: "INPUT_ERROR: combination doesn't match any shipped print setting"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene type not supported"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/scale/:
    post:
      operationId: scaleModels
      summary: Scale models
      tags:
        - Modifying Models
      description: |
        Set a uniform scale factor on one or more models. `scale` replaces
        each model's current scale. It is not multiplied against the model's
        existing scale.

        Scaling a model does not re-validate the layout. A model may end up
        outside the build volume, which `/v1/scene/model/validate/in-bounds/`
        checks, or overlapping another model, which
        `/v1/scene/model/validate/interferences/` checks.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body does not match the request schema: `scene_id` or `scale` missing, a field has the wrong type, or an unknown field is present. The message includes a JSON pointer to the offending field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScaleRequest'
            examples:
              ScaleOneModel:
                summary: Scale one model to 150%
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  scale: 1.5
              ScaleAllModels:
                summary: Scale every model in the scene
                description: Omitting `model_ids` selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  scale: 0.5
      responses:
        "200":
          description: >
            The models were scaled. Reports which models were scaled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModifiedModels'
              examples:
                OneModelScaled:
                  summary: One model scaled, still inside the build volume
                  value:
                    modified_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                MissingScale:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'scale' not found in reference to key path: /"
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Invalid JSON input"
        "404":
          description: >
            A model in `model_ids` does not exist in the scene. No models
            were modified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/orient/:
    post:
      operationId: orientModels
      summary: Orient models
      tags:
        - Modifying Models
      description: |
        Set the absolute orientation of one or more models, replacing their
        current orientation (not a relative rotation). Provide `orientation`
        as exactly one of: Euler angles, a 3x3 rotation matrix, or a pair of
        direction vectors. The response always reports the resulting
        orientation as Euler angles, regardless of which form was used in
        the request.

        Orienting a model does not re-validate the layout. A model may end
        up outside the build volume, which `/v1/scene/model/validate/in-bounds/`
        checks, or overlapping another model, which
        `/v1/scene/model/validate/interferences/` checks.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body does not match the request schema: `scene_id` or `orientation` missing, `orientation` matches none of its three shapes, a field has the wrong type, or an unknown field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID or no scene with that id exists, or `orientation.linear` is not a pure rotation matrix. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrientRequest'
            examples:
              EulerAngles:
                summary: Orient with Euler angles
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  orientation: {x: 0.0, y: 0.0, z: 30.0}
              TransformMatrix:
                summary: Orient with a rotation matrix
                description: >
                  A pure 45 degree rotation about the z axis, as a
                  row-major 3x3 matrix.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  orientation:
                    linear:
                      - [0.70710678118, -0.70710678118, 0.0]
                      - [0.70710678118, 0.70710678118, 0.0]
                      - [0.0, 0.0, 1.0]
              DirectionVectors:
                summary: Orient with direction vectors
                description: >
                  Points the model's own +Z axis straight up, and its own
                  +X axis to the right, in scene space.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  orientation:
                    z_direction: [0.0, 0.0, 1.0]
                    x_direction: [1.0, 0.0, 0.0]
      responses:
        "200":
          description: >
            The models were oriented. Reports which models were oriented.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModifiedModels'
              examples:
                OneModelOriented:
                  summary: One model oriented, still inside the build volume
                  value:
                    modified_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
        "400":
          description: >
            The request was malformed, referenced a missing scene, or the
            orientation matrix was degenerate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                NotARotationMatrix:
                  summary: "INPUT_ERROR: linear is not a pure rotation matrix"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "linear matrix must be a pure rotation matrix"
                MissingOrientation:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'orientation' not found in reference to key path: /"
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Invalid JSON input"
        "404":
          description: >
            A model in `model_ids` does not exist in the scene. No models
            were modified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/translate/:
    post:
      operationId: translateModels
      summary: Translate models
      tags:
        - Modifying Models
      description: |
        Move one or more models to a new position, either absolutely (place
        the model origin at the given coordinates) or relatively (offset each
        model from where it currently is).

        Positions are in the scene's user coordinate space, in millimeters:
        `x`/`y` are horizontal with `(0, 0)` at the center of the build
        platform, and `z` is vertical with `0` at the platform surface. On SLA
        scenes the `z` component is ignored. Models stay attached to the build
        platform (or their raft height).

        Translating a model does not re-validate the layout. A model may be
        moved outside the build volume, which `/v1/scene/model/validate/in-bounds/`
        checks, or into another model, which
        `/v1/scene/model/validate/interferences/` checks.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body does not match the request schema: `scene_id` or `position` missing, a field has the wrong type, `mode` is not one of the documented values, or an unknown field is present. The message includes a JSON pointer to the offending field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TranslateRequest'
            examples:
              MoveOneModelAbsolute:
                summary: Place one model at exact coordinates
                description: >
                  Puts the model's origin at x=30, y=-15. The z value is
                  required by the schema but ignored on SLA scenes.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  position: {x: 30.0, y: -15.0, z: 0.0}
                  mode: "ABSOLUTE"
              NudgeAllModelsRelative:
                summary: Shift every model in the scene by an offset
                description: >
                  Omitting `model_ids` selects all models. RELATIVE mode adds
                  the offset to each model's current position, so models keep
                  their arrangement.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  position: {x: 0.0, y: 25.0, z: 0.0}
                  mode: "RELATIVE"
      responses:
        "200":
          description: >
            The models were translated. Reports which models were moved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModifiedModels'
              examples:
                OneModelMoved:
                  summary: One model moved, still inside the build volume
                  value:
                    modified_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                MissingPosition:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'position' not found in reference to key path: /"
                InvalidMode:
                  summary: "JSON_SCHEMA_MATCH_ERROR: value outside the enum"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "instance is not one of the enumerated values in reference to key path: /mode"
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Invalid JSON input"
        "404":
          description: >
            A model in `model_ids` does not exist in the scene. No models
            were modified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/auto-orient/:
    post:
      operationId: autoOrientModels
      summary: Automatically orient models
      tags:
        - Modifying Models
      description: |
        Reorients one or more models to the orientation PreForm estimates
        will produce the best print quality on the scene's printer. Pass
        `mode: "DENTAL"` to use the dental-specific algorithm instead, which
        tilts the appliance's occlusal (biting) plane by `tilt` degrees
        (default `0`) rather than searching for a quality-optimized
        orientation.

        A reoriented model can end up outside the build volume or
        overlapping another model. Check with
        `/v1/scene/model/validate/in-bounds/` and
        `/v1/scene/model/validate/interferences/`.

        This can take a while, especially with the default algorithm across
        many models, so prefer `?async=true` and poll the returned operation.

        Unlike `/v1/scene/model/scale/` and `/v1/scene/model/orient/`, an
        unknown id in `model_ids` is reported as `OPERATION_FAILED` rather
        than `NOT_FOUND`.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body matches neither request shape: `scene_id` is missing, `mode` is present but not `"DENTAL"`, `tilt` is present without `mode: "DENTAL"`, a field has the wrong type, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | A model in `model_ids` does not exist in the scene, or the auto-orient algorithm itself failed. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutoOrientRequest'
            examples:
              AllModelsDefault:
                summary: Auto-orient every model in the scene
                description: Omitting `model_ids` selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              SpecificModels:
                summary: Auto-orient specific models
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
              DentalMode:
                summary: Auto-orient with the dental algorithm, tilted 30 degrees
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  mode: "DENTAL"
                  tilt: 30
      responses:
        "200":
          description: >
            The models were auto-oriented. Reports which models were
            reoriented.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModifiedModels'
              examples:
                Oriented:
                  summary: Two models auto-oriented
                  value:
                    modified_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10", "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"]
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the operation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                ModelNotFound:
                  summary: "OPERATION_FAILED: unknown model id"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Specified model could not be found in the Scene."
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/auto-support/:
    post:
      operationId: autoSupportModels
      summary: Generate supports for models
      tags:
        - Modifying Models
      description: |
        Computes and attaches print supports to one or more models, using
        the scene's current support settings as defaults for any field
        omitted from the request. Existing supports on a targeted model
        are replaced.

        Supports and their raft count toward whether a model fits, so a
        model that fit before can extend past the build volume, or into
        another model, once it is supported. Check with
        `/v1/scene/model/validate/in-bounds/` and
        `/v1/scene/model/validate/interferences/`.

        `only_minima` generates only the touchpoints a model strictly
        needs to print (a faster, reduced-fidelity pass) instead of the
        full support structure.

        `breakaway_structure_enabled` is only accepted when the scene's
        material supports breakaway supports. Otherwise it is rejected
        with `INPUT_ERROR`.

        This can take a while across many models, so prefer `?async=true`
        and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id` is missing, a numeric field is negative, `raft_type` is outside its enum, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID or no scene with that id exists, or `breakaway_structure_enabled` was given but the scene's material doesn't support breakaway supports. |
        | 400 | `OPERATION_FAILED` | A model in `model_ids` does not exist in the scene, the scene's printer type doesn't support generating supports, a model is hidden/not printable or too large to support, or support generation itself failed. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutoSupportRequest'
            examples:
              AllModelsDefault:
                summary: Auto-support every model with the scene's current settings
                description: Omitting model_ids selects all models. Omitted settings keep their current values.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              CustomSettings:
                summary: Auto-support specific models with custom density and a mini raft
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  density: 1.2
                  raft_type: "MINI_RAFT"
                  only_minima: false
      responses:
        "200":
          description: >
            Supports were generated. Reports which models were supported.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModifiedModels'
              examples:
                Supported:
                  summary: One model supported
                  value:
                    modified_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the operation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                BreakawayNotSupported:
                  summary: "INPUT_ERROR: breakaway supports not available in this material"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "breakaway_structure_enabled setting is not supported in the current material"
                ModelNotFound:
                  summary: "OPERATION_FAILED: unknown model id"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Model could not be found."
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/regenerate-supports/:
    post:
      operationId: regenerateSupports
      summary: Rebuild existing supports in place
      tags:
        - Modifying Models
      description: |
        Rebuilds the support structure of models that already have
        supports, keeping their touchpoints and the support settings they
        were generated with. Only what is derived from the model itself
        changes; in particular a raft generated with `raft_label_enabled`
        is re-engraved with the model's current name. This is how to clear
        the `RAFT_LABEL_OUTDATED` warning `/v1/scene/model/update/` reports
        after a rename without recomputing touchpoints. To change settings
        or touchpoints instead, use `/v1/scene/model/auto-support/`.

        Omitting `model_ids` (or sending an empty array) selects every model
        in the scene that has supports. Listing a model without supports is
        an error; nothing is regenerated in that case.

        Rebuilt supports and their raft count toward whether a model fits,
        so a regenerated model can extend past the build volume or into
        another model. Check with `/v1/scene/model/validate/in-bounds/` and
        `/v1/scene/model/validate/interferences/`.

        Regeneration skips touchpoint finding, the most expensive part of
        `/v1/scene/model/auto-support/`, so it is much cheaper than
        generating supports from scratch. Rebuilding the support structure
        still takes noticeable time for large models or many models, so
        prefer `?async=true` and poll the returned operation. If any
        regeneration fails, the whole request fails and no model is changed.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id` is missing, `model_ids` is not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, no scene with that id exists, or a model in `model_ids` has no supports to regenerate. No models were modified. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. No models were modified. |
        | 400 | `OPERATION_FAILED` | Support generation itself failed for a model. No models were modified. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegenerateSupportsRequest'
            examples:
              OneModel:
                summary: Re-engrave one renamed model's raft label
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
              AllSupportedModels:
                summary: Rebuild the supports of every supported model
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: The supports were regenerated. Reports which models were regenerated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModifiedModels'
              examples:
                Regenerated:
                  value:
                    modified_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene or an unsupported model, or the regeneration failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                ModelHasNoSupports:
                  summary: "INPUT_ERROR: a listed model has no supports"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 has no supports to regenerate"
                RegenerationFailed:
                  summary: "OPERATION_FAILED: support generation failed"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Couldn't generate support structure."
        "404":
          description: A model in `model_ids` does not exist in the scene. No models were modified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/auto-layout/:
    post:
      operationId: autoLayoutModels
      summary: Automatically arrange models on the build platform
      tags:
        - Modifying Models
      description: |
        Repositions and rotates one or more models to fit them on the
        build platform. This endpoint is for SLA build platforms. Use
        `/v1/scene/model/auto-pack/` for SLS scenes.

        Pass `mode: "DENTAL"` to temporarily use the scene's dental
        workspace settings for the layout instead of the standard ones.

        By default (`on_layout_failure: "FAIL_AND_LEAVE_SCENE_UNCHANGED"`), a layout
        that can't fit every requested model fails the whole request and
        leaves the scene unchanged. Set
        `on_layout_failure: "EJECT_A_MODEL_AND_RETRY"` to instead have the
        algorithm greedily remove seeded-random models from the scene,
        retrying until the rest fit (capped by `max_ejections`, if
        given). The response reports the outcome only: `laid_out_model_ids`
        and `ejected_model_ids`, in the request's `model_ids` order.
        `ejected_model_ids` is empty unless a model was actually ejected.
        Fetch `/v1/scene/model/list/` afterwards for the resulting poses.

        Passing `seed` makes the chosen arrangement reproducible across
        calls with the same inputs. If `seed` is given without
        `layout_attempts`/`iterations_per_attempt`, both are pinned to
        fixed defaults so results stay deterministic. Without `seed`, a
        random seed is used and results vary across calls.

        This can take a while, especially with many models or high
        `layout_attempts`/`iterations_per_attempt`, so prefer `?async=true`
        and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id` is missing, `mode`/`on_layout_failure` is outside its enum, `custom_bounds` is missing one of its four fields, `seed` exceeds the uint32 range, `layout_attempts`/`iterations_per_attempt` is outside its range, `max_ejections` is present without `on_layout_failure: "EJECT_A_MODEL_AND_RETRY"` or is not a positive integer, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | A model in `model_ids` does not exist in the scene, the scene's printer type doesn't support auto-layout, or (only in `FAIL_AND_LEAVE_SCENE_UNCHANGED` mode) not every requested model fits within the build volume. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutoLayoutRequest'
            examples:
              AllModelsDefault:
                summary: Lay out every model with default spacing
                description: Omitting model_ids selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              SeededWithEjection:
                summary: Reproducible layout that ejects models rather than failing
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10", "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"]
                  seed: 42
                  on_layout_failure: "EJECT_A_MODEL_AND_RETRY"
                  max_ejections: 1
      responses:
        "200":
          description: >
            The models were laid out (and possibly ejected). Reports which
            requested models remain in the scene and which were ejected.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoLayoutResponse'
              examples:
                Fit:
                  summary: Every requested model fit
                  value:
                    laid_out_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                    ejected_model_ids: []
                Ejected:
                  summary: "on_layout_failure EJECT_A_MODEL_AND_RETRY: one model was ejected"
                  value:
                    laid_out_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                    ejected_model_ids: ["a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"]
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the operation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                MaxEjectionsWithoutMode:
                  summary: "JSON_SCHEMA_MATCH_ERROR: max_ejections without ejection mode"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'on_layout_failure'."
                ModelNotFound:
                  summary: "OPERATION_FAILED: unknown model id"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Some model(s) couldn't be found."
                DoesNotFit:
                  summary: "OPERATION_FAILED: not every model fits (FAIL_AND_LEAVE_SCENE_UNCHANGED mode)"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Not all models fit within build volume"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/auto-pack/:
    post:
      operationId: autoPackModels
      summary: Automatically pack models within the build volume
      tags:
        - Modifying Models
      description: |
        Packs the selected models within the build volume without
        duplicating them, using 3D nesting rather than 2D placement.
        Unselected models are left in place and act as fixed obstacles.
        This endpoint is for SLS printers. Use
        `/v1/scene/model/auto-layout/` for SLA scenes.

        The packer is greedy: it places models one at a time and carries on
        when one does not fit. `on_pack_failure` decides what happens to the
        models it could not place. With `FAIL_AND_LEAVE_SCENE_UNCHANGED` (the default) the
        request fails and the scene is left unchanged. With `PARK_OUTSIDE_BUILD_VOLUME`
        the packed models keep their new poses, the models that did not fit
        are parked beside the build volume, outside it, and the response
        names both groups, so a caller can drain a queue that is larger than
        one build: pack, keep what landed, carry the rest to the next scene.

        The response reports the outcome only: `packed_model_ids` and
        `unpacked_model_ids` covering every selected visible model (hidden
        models are never packed, so they are not reported). Fetch
        `/v1/scene/model/list/` afterwards for the resulting poses.

        This can take a while, so prefer `?async=true` and poll the
        returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id` is missing, `seed` exceeds the uint32 range, `distance_from_wall_mm` is negative, `packing_mode` or `on_pack_failure` is outside its enum, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
        | 400 | `OPERATION_FAILED` | The scene's printer type doesn't support packing, or, under `on_pack_failure: FAIL_AND_LEAVE_SCENE_UNCHANGED`, not every model fits within the build volume. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutoPackRequest'
            examples:
              AllModelsDefault:
                summary: Pack every model with default spacing
                description: Omitting model_ids selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              ParkOutsideBuildVolume:
                summary: Pack as many as fit and report the rest
                description: >
                  Models the packer cannot place are left outside the build
                  volume and listed in `unpacked_model_ids` instead of
                  failing the request.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  on_pack_failure: "PARK_OUTSIDE_BUILD_VOLUME"
                  seed: 7
              SpecificModelsVolumeMode:
                summary: Pack specific models, minimizing packed volume
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  packing_mode: "PACK_VOLUME"
                  distance_from_wall_mm: 5.0
      responses:
        "200":
          description: >
            The pack ran. Reports which of the selected models were placed
            inside the build volume and which were left outside it. Under
            `FAIL_AND_LEAVE_SCENE_UNCHANGED`, `unpacked_model_ids` is always empty here,
            because a pack that leaves anything outside fails instead.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutoPackResponse'
              examples:
                AllPlaced:
                  summary: Every selected model fit
                  value:
                    packed_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10", "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"]
                    unpacked_model_ids: []
                SomeLeftOutside:
                  summary: "on_pack_failure PARK_OUTSIDE_BUILD_VOLUME: one model did not fit"
                  value:
                    packed_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                    unpacked_model_ids: ["9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"]
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the operation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                DoesNotFit:
                  summary: "OPERATION_FAILED: not every model fits (on_pack_failure FAIL_AND_LEAVE_SCENE_UNCHANGED)"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Not all models fit within build volume"
                UnsupportedMachine:
                  summary: "OPERATION_FAILED: packing isn't supported for this printer type"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Packing not supported for given MachineType."
        "404":
          description: A model in `model_ids` does not exist in the scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/fill-build-platform/:
    post:
      operationId: fillBuildPlatform
      summary: Fill the build platform with duplicates
      tags:
        - Modifying Models
      description: |
        Duplicates the selected models and lays out the copies (using the
        same algorithm as `/v1/scene/model/auto-layout/`), adding as many
        as fit on the build platform. Unselected models are left in place
        and act as fixed obstacles. This endpoint is for SLA.
        Use `/v1/scene/model/fill-build-chamber/` for SLS scenes.

        Returns `new_model_ids`. Call `/v1/scene/model/list/` afterwards
        to see the full scene result.

        This can take a while, so prefer `?async=true` and poll the
        returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id` is missing, `layout_attempts`/`iterations_per_attempt` is outside its range, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | A model in `model_ids` does not exist in the scene, the scene's printer type doesn't support this operation, or not every duplicate fits within the build volume. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FillBuildPlatformRequest'
            examples:
              AllModelsDefault:
                summary: Fill the platform with copies of every model
                description: Omitting model_ids selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              OneModelCustomSpacing:
                summary: Fill with copies of one model, spaced 5mm apart
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  layout_options:
                    model_spacing_mm: 5.0
      responses:
        "200":
          description: >
            Duplicates were added and laid out. Returns the ids of the
            newly created models. The scene itself is not included.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NewModelIds'
              examples:
                Filled:
                  value:
                    new_model_ids:
                      - "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
                      - "b2c3d4e5-f6a7-4890-b1c2-d3e4f5a6b7c8"
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the operation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                ModelNotFound:
                  summary: "OPERATION_FAILED: unknown model id"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Some model(s) couldn't be found."
                DoesNotFit:
                  summary: "OPERATION_FAILED: not every duplicate fits"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Not all models fit within build volume"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/fill-build-chamber/:
    post:
      operationId: fillBuildChamber
      summary: Fill the build chamber with duplicates
      tags:
        - Modifying Models
      description: |
        Duplicates the selected models and packs them (the same algorithm
        as `/v1/scene/model/auto-pack/`) up to `fill_to_height_mm`, in mm
        above the build platform. Unselected models are left in place and
        act as fixed obstacles. This endpoint is for SLS. Use
        `/v1/scene/model/fill-build-platform/` for SLA scenes.

        Returns `new_model_ids`. Call `/v1/scene/model/list/` afterwards
        to see the full scene result.

        This can take a while, so prefer `?async=true` and poll the
        returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id` is missing, `fill_to_height_mm` is not positive, `seed` exceeds the uint32 range, `distance_from_wall_mm` is negative, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
        | 400 | `OPERATION_FAILED` | The scene's printer type doesn't support packing, or not every duplicate fits within `fill_to_height_mm`. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FillBuildChamberRequest'
            examples:
              WholeChamberDefault:
                summary: Fill the whole build chamber with copies of every model
                description: >-
                  Omitting model_ids selects all models. Omitting
                  fill_to_height_mm fills to the top of the build volume.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              FillToHeight:
                summary: Fill one model's copies up to 42mm
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  fill_to_height_mm: 42.0
                  packing_options:
                    seed: 12345
      responses:
        "200":
          description: >
            Duplicates were added and packed. Returns the ids of the newly
            created models. The scene itself is not included.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NewModelIds'
              examples:
                Filled:
                  value:
                    new_model_ids:
                      - "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
                      - "b2c3d4e5-f6a7-4890-b1c2-d3e4f5a6b7c8"
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the operation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                InvalidFillHeight:
                  summary: "JSON_SCHEMA_MATCH_ERROR: fill_to_height_mm not positive"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "fill_to_height_mm must be positive"
                DoesNotFit:
                  summary: "OPERATION_FAILED: not every duplicate fits by fill_to_height_mm"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Not all models fit within build volume"
        "404":
          description: A model in `model_ids` does not exist in the scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/import/:
    post:
      operationId: importModels
      summary: Import a model
      tags:
        - Modifying Models
      description: |
        Loads a 3D model file into the scene as a new model. Supported
        mesh formats are STL, OBJ, and 3MF, and supported CAD formats
        are STEP, Solidworks, PTC Creo, JT, Catia V5, Inventor, and Rhino.
        `chordal_tolerance_um` and `angle_tolerance_deg` control
        tessellation and only affect CAD imports. Saved PreForm projects
        (`.form`) are not model files and are rejected with `INPUT_ERROR`;
        open them as new scenes with `/v1/scene/open/`.

        The model is placed using the file's own coordinates and is never
        moved away from models it overlaps; check for that with
        `/v1/scene/model/validate/interferences/` after importing. The
        scene's build-volume and snapping rules apply, so on printers that
        snap to the build platform the model is dropped onto it, and a model
        outside the build volume is moved inside. A model too large to fit
        stays out of bounds; check with `/v1/scene/model/validate/in-bounds/`. Change its pose afterwards
        through the dedicated scale/orient/translate endpoints. `name`
        overrides the display name that would otherwise be derived from the
        file name.

        > **Multi-part files import as one model.** A CAD or 3MF file that
        > holds several parts is imported as a single combined model unless
        > the request sets `split_multi_model_file: true`.

        With `split_multi_model_file: true`, `new_models` has one entry per
        created model, in unspecified order. The per-model overrides
        (`name`, `lock`) target a single model, so they are accepted only
        when the import creates exactly one model. When a split produces
        several parts the request fails with `INPUT_ERROR` and nothing is
        imported; drop the overrides or import without splitting.

        The response identifies each new model by `model_id` but does not
        describe it. To read the imported models' properties, such as pose,
        bounding box, and `in_bounds`, call `/v1/scene/model/list/` with the
        same `scene_id` and `model_ids` set to the `model_id` values from
        `new_models`. To read the whole scene instead, call `/v1/scene/get/`
        with the `scene_id`. Both are `POST` requests that take their input
        as a JSON body:

        ```json
        POST /v1/scene/model/list/
        {
          "scene_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
          "model_ids": ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
        }
        ```

        `file` must be a model file, matched on the extension regardless of
        case; the extension is checked before the ids.

        Import can take a while for large or CAD files, so prefer
        `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id` or `file` is missing or not a string, `repair_behavior`/`units` is outside its enum, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `file`'s extension is not a supported model format (`.form` files are rejected here; open them with `/v1/scene/open/`), a per-model override (`name`, `lock`) was given and the file produced more than one model (nothing is imported), `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | The import failed for a reason not covered above: the file does not exist, is corrupt, holds no model, or `repair_behavior` is `ERROR` and the model needs repair. |
        | 500 | `UNKNOWN_FAILURE` | An unexpected exception occurred inside PreFormServer. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportModelRequest'
            examples:
              Minimal:
                summary: Import a file with default placement and settings
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  file: "C:\\Models\\bracket.stl"
              AllFields:
                summary: Import with an explicit name and repair behavior
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  file: "C:\\Models\\bracket.step"
                  repair_behavior: "REPAIR"
                  name: "bracket-v2"
                  units: "MILLIMETERS"
                  lock: "FREE"
                  chordal_tolerance_um: 10
                  angle_tolerance_deg: 5
              SplitMultiModelFile:
                summary: Import a multi-part file as one model per part
                description: >
                  split_multi_model_file cannot be combined with the
                  per-model overrides (name, lock).
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  file: "C:\\Models\\assembly.step"
                  split_multi_model_file: true
      responses:
        "200":
          description: >
            The file was imported. `new_models` has one entry per created
            model (exactly one unless `split_multi_model_file: true` split
            the file into several).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportModelResult'
              examples:
                Imported:
                  value:
                    new_models:
                      - model_id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                        source_part_name: "bracket"
                        source_part_index: 0
                SplitAssembly:
                  summary: A two-part 3MF split into two models
                  value:
                    new_models:
                      - model_id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                        source_part_name: "housing"
                        source_part_index: 0
                      - model_id: "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
                        source_part_name: "base plate"
                        source_part_index: 1
        "202":
          description: >
            The import was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the returned
            id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the import failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingFile:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'file' not found in reference to key path: /"
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'scene_id' not found in reference to key path: /"
                UnrecognizedField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: unknown field in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Object contains a property that could not be validated using 'properties' or 'additionalProperties' constraints: 'bogus_field'."
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                UnsupportedFileType:
                  summary: "INPUT_ERROR: file extension not supported"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "File type not supported."
                ImportFailed:
                  summary: "OPERATION_FAILED: the file could not be imported"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Model file not found: C:\\Models\\bracket.stl"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/list/:
    post:
      operationId: listModels
      summary: List models in a scene
      tags:
        - Modifying Models
      description: |
        Returns the current properties of the models in a scene. Supply the
        optional `model_ids` array to restrict the response to those models.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListModelsRequest'
            examples:
              AllModels:
                summary: List every model in the scene
                description: Omitting model_ids selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              SpecificModels:
                summary: List two specific models
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
      responses:
        "200":
          description: >
            Returns the requested models, in the order given by `model_ids`.
            If `model_ids` is omitted or empty, returns every model in the
            scene, in a deterministic but otherwise arbitrary order.
          content:
            application/json:
              schema:
                type: object
                required: [models]
                properties:
                  models:
                    type: array
                    items:
                      $ref: '#/components/schemas/ModelProperties'
              examples:
                OneModel:
                  value:
                    models:
                      - id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                        name: "bracket.stl"
                        position: {x: 0.0, y: 0.0, z: 5.2}
                        orientation: {x: 0.0, y: 0.0, z: 0.0}
                        scale: 1.0
                        units: "MILLIMETERS"
                        bounding_box:
                          min_corner: {x: -17.9, y: -32.9, z: 0.0}
                          max_corner: {x: 17.9, y: 2.9, z: 41.7}
                        original_file: "C:\\Models\\bracket.stl"
                        visible: true
                        has_supports: true
                        is_group: false
                        is_cage: false
                        in_bounds: true
                        raw_mesh_hash: "5f2b0c9a8d3e41f7"
                        canonical_model_hash: "9c81f0b2a6d45e37"
                        lock: "FREE"
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                InvalidModelIds:
                  summary: "JSON_SCHEMA_MATCH_ERROR: model_ids is not an array"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Value type not permitted by 'type' constraint. in reference to key path: /model_ids"
                SceneNotFound:
                  summary: "INPUT_ERROR: missing or unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        "404":
          description: A model in `model_ids` does not exist in the scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model with given model_id not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/update/:
    post:
      operationId: updateModels
      summary: Update model metadata
      tags:
        - Modifying Models
      description: |
        Edits the metadata for one or more models. This includes renaming
        them, or changing their unit interpretation or layout lock.

        Each field given (`name`, `units`, `lock`) applies its single value
        to every model specified by `model_ids`. Omitting `model_ids`
        applies the changes to every model in the scene.

        Changing `units` rescales the model's geometry, so a model can grow
        past the build volume or into another model. Check with
        `/v1/scene/model/validate/in-bounds/` and
        `/v1/scene/model/validate/interferences/`.

        Renaming does not touch a model's supports. A raft generated with
        `raft_label_enabled` engraves the name the model had at generation
        time, so once a model is renamed its raft spells a different name
        until its supports are regenerated. Every model this request renames
        whose raft carries such a label is reported in `warnings` as
        `RAFT_LABEL_OUTDATED`; call `/v1/scene/model/regenerate-supports/`
        to re-engrave the raft with the current name, or
        `/v1/scene/model/auto-support/` to generate supports afresh.

        PreForm does not record the engraved text, so the warning describes
        this request, not the raft: renaming a model back to the name its
        raft was generated with still warns, and a rename to the model's
        current name does not. It is also only raised where a label exists:
        supports without a raft, mini rafts on the build platform
        (`MINI_RAFTS_ON_BP`) and legacy V01 rafts carry none, whatever the
        knob says.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id` is missing, `name`/`units`/`lock`/`model_ids` has the wrong type, `units` or `lock` is outside its enum, a pose field (`scale`/`position`/`orientation`) is present, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. No models were modified. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateModelRequest'
            examples:
              Rename:
                summary: Rename one model
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  name: "bracket-final"
              LockAllModels:
                summary: Fully lock every model in the scene
                description: Omitting model_ids selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  lock: "FULLY_LOCKED"
      responses:
        "200":
          description: >
            The models were updated. Reports which models were updated, and
            any `warnings` about side effects the update did not perform.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateModelResult'
              examples:
                Renamed:
                  summary: One model renamed
                  value:
                    modified_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                    warnings: []
                RenamedWithOutdatedRaftLabel:
                  summary: Renamed a model whose raft still engraves the previous name
                  value:
                    modified_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                    warnings:
                      - code: "RAFT_LABEL_OUTDATED"
                        model_id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
        "400":
          description: The request was malformed, or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                PoseFieldRejected:
                  summary: "JSON_SCHEMA_MATCH_ERROR: a pose field was included"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Object contains a property that could not be validated using 'properties' or 'additionalProperties' constraints: 'scale'."
        "404":
          description: A model in `model_ids` does not exist in the scene. No models were modified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/delete/:
    delete:
      operationId: deleteModels
      summary: Delete models
      tags:
        - Modifying Models
      description: |
        Removes one or more models from a scene. Models are validated
        before any are deleted: if any requested model does not exist,
        nothing is deleted.

        **Omitting `model_ids` (or sending an empty array) deletes every
        model in the scene.**

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. Nothing was deleted. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteModelRequest'
            examples:
              SpecificModels:
                summary: Delete two specific models
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids:
                    - "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                    - "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
              AllModels:
                summary: Delete every model in the scene
                description: Omitting model_ids deletes all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: The models were deleted. The response has no body.
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                InvalidModelIds:
                  summary: "JSON_SCHEMA_MATCH_ERROR: model_ids is not an array"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Value type not permitted by 'type' constraint. in reference to key path: /model_ids"
                SceneNotFound:
                  summary: "INPUT_ERROR: missing or unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        "404":
          description: A model in `model_ids` does not exist in the scene. Nothing was deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/replace/:
    post:
      operationId: replaceModel
      summary: Replace a model with another model already in the scene
      tags:
        - Modifying Models
      description: |
        Swaps one model in the scene for another. The model identified by
        `replacement_model_id` takes over the pose (position, orientation,
        scale, units) of the model identified by `model_id`, and `model_id`
        is removed from the scene. The replacement keeps its own id and its
        own layout lock; set the lock afterwards with `/v1/scene/model/update/` if it should match.

        If the replaced model had supports, PreForm attempts to recompute
        them for the replacement's geometry using the replaced model's
        support settings and contact locations. Any supports the replacement
        already carried are discarded. `warnings` in the response reports
        when the carry-over could not be done completely; the model may then
        need supports regenerated with `/v1/scene/model/auto-support/`.

        Taking over the replaced model's pose, and any recomputed supports,
        can leave the replacement outside the build volume or overlapping
        another model. Check with `/v1/scene/model/validate/in-bounds/` and
        `/v1/scene/model/validate/interferences/`.

        If the replaced model had a user-provided name and the replacement's
        name was generated from its file, the replacement takes the replaced
        model's name.

        This endpoint never reads a file. To emulate PreForm's desktop
        "Replace Model" action, which loads a new version of a part from
        disk, chain two calls: `/v1/scene/model/import/` the new file into
        the same scene, then call this endpoint with `model_id` set to the
        old model and `replacement_model_id` set to the id `import` returned.
        The import places the model with the scene's default snapping; the
        replacement then moves it onto the old model's pose, so where the
        import put it does not matter. If the replacement fails, the imported
        model remains in the scene and can be deleted with
        `/v1/scene/model/delete/`. Because the pose carries units, a file
        imported with default units ends up interpreted in the replaced
        model's units, exactly as the desktop action does.

        Support regeneration can take a while, so prefer `?async=true` and
        poll the returned operation. Neither model is changed until the whole
        replacement has succeeded.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: `scene_id`, `model_id` or `replacement_model_id` is missing or not a string, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `model_id` or `replacement_model_id` is not a well-formed UUID, the two ids are equal, `scene_id` is not a well-formed UUID, or no scene with that id exists. Ids are checked in that order, before either model is looked up. |
        | 404 | `NOT_FOUND` | `model_id` or `replacement_model_id` is well-formed but does not exist in the scene; the message names the missing id. The scene is not changed. |
        | 400 | `OPERATION_FAILED` | The replacement could not be applied: on the replaced model's pose the replacement's geometry would lie outside the build volume or be thinner than one layer, or support regeneration failed outright. The scene is not changed. |
        | 500 | `UNKNOWN_FAILURE` | An unexpected exception occurred inside PreFormServer. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReplaceModelRequest'
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                  replacement_model_id: "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
      responses:
        "200":
          description: >
            The model was replaced: the replacement now has the replaced
            model's pose, and `model_id` is gone from the scene. `warnings`
            reports any problem carrying the supports over.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReplaceModelResult'
              examples:
                Replaced:
                  value:
                    warnings: []
                ReplacedWithWarning:
                  summary: Supports could not be fully carried over to the replacement geometry
                  value:
                    warnings: ["NOT_ALL_CONTACT_LOCATIONS_APPLIED"]
        "202":
          description: >
            The replacement was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the returned
            id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the replacement could not be applied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingReplacementModelId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'replacement_model_id' not found in reference to key path: /"
                UnrecognizedField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: unknown field in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Object contains a property that could not be validated using 'properties' or 'additionalProperties' constraints: 'bogus_field'."
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: required field absent"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'scene_id' not found in reference to key path: /"
                SameModel:
                  summary: "INPUT_ERROR: a model cannot replace itself"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "replacement_model_id must differ from model_id"
                ReplaceFailed:
                  summary: "OPERATION_FAILED: the replacement does not fit on the replaced model's pose"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "The model could not be replaced as the imported model is too large."
        "404":
          description: >
            `model_id` or `replacement_model_id` does not exist in the scene.
            The scene is not changed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
                ReplacementNotFound:
                  summary: "NOT_FOUND: unknown replacement id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Replacement model a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/duplicate/:
    post:
      operationId: duplicateModels
      summary: Duplicate models
      tags:
        - Modifying Models
      description: |
        Creates `count` copies of each selected model, placed alongside the
        originals. Models are validated before any are duplicated: if any
        requested model does not exist, nothing is duplicated.

        Returns one `results` entry per selected model, in request order,
        naming the ids of the copies created from it (in no particular
        order).

        Only models with their own geometry can be duplicated: selecting a
        group or cage model fails with `INPUT_ERROR`, and nothing is
        duplicated.

        `count` is capped at 1000 per selected model to bound how much a
        single request can add to a scene.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` or `count` is missing, or `count` is not an integer or is outside 1-1000. Also returned when `model_ids` is present but not an array of strings, or when an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. Also returned when a model in `model_ids` is a group or cage with no geometry of its own. Nothing was duplicated. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. Nothing was duplicated. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DuplicateModelRequest'
            examples:
              OneModel:
                summary: Create 2 extra copies of one model
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  count: 2
              AllModels:
                summary: Duplicate every model in the scene once
                description: Omitting model_ids selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  count: 1
      responses:
        "200":
          description: >
            The models were duplicated. `results` has one entry per selected
            model, in request order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DuplicateModelsResult'
              examples:
                TwoCopiesOfOneModel:
                  summary: count was 2 for a single selected model
                  value:
                    results:
                      - source_model_id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                        new_model_ids:
                          - "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
                          - "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                CountOutOfRange:
                  summary: "JSON_SCHEMA_MATCH_ERROR: count outside 1-1000"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "instance is below minimum of 1 in reference to key path: /count"
                SceneNotFound:
                  summary: "INPUT_ERROR: missing or unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        "404":
          description: A model in `model_ids` does not exist in the scene. Nothing was duplicated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/cage/:
    post:
      operationId: cageModels
      summary: Cage models
      tags:
        - Modifying Models
      description: |
        Generates a support cage around the selected models: a bar-lattice
        frame that encloses them so they can be handled and depowdered as
        one rigid unit, protecting small or fragile parts from being lost
        or damaged. Any existing cage or group among the selected models is
        replaced.

        Fields left unspecified fall back to the scene's default
        cage-generation settings. `enable_square_bars: true` (the default)
        gives the bars a rectangular cross-section (`bar_width_mm` by
        `bar_thickness_mm`). `false` gives them a round cross-section.

        Caging is an SLS-only feature. Caging a scene on an unsupported (SLA)
        machine fails with `OPERATION_FAILED`.

        Unlike the other model-modifying endpoints, this one does not
        support `?async=true`.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `model_ids` is present but not an array of strings, a bar/distance field is below its minimum, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. Nothing is caged. |
        | 400 | `OPERATION_FAILED` | The scene's machine type does not support part caging, not all models fit within the build volume once the cage is added, or cage generation produced no cage model. The message carries the underlying reason. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CageRequest'
            examples:
              Default:
                summary: Cage two models with the scene's default bar settings
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids:
                    - "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                    - "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
              CustomBars:
                summary: Cage with custom bar geometry and an explicit label
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  cage_label: "Cage - batch 12"
                  bar_spacing_mm: 8.0
                  bar_thickness_mm: 2.0
                  bar_width_mm: 2.0
                  distance_to_cage_mm: 3.0
                  enable_round_edges: true
                  enable_square_bars: false
      responses:
        "200":
          description: >
            The cage was created. `cage_model_id` is the id of the new cage
            model (`is_cage: true`, `is_group: true` in
            `/v1/scene/model/list/`). The selected models are now its
            children.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CageResult'
              examples:
                Caged:
                  value:
                    cage_model_id: "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
        "400":
          description: The request was malformed, referenced a missing scene, or caging failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                NotSupported:
                  summary: "OPERATION_FAILED: machine type does not support caging"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Part Caging is not supported for the given machine type: FUSE-1-0"
                DoesNotFit:
                  summary: "OPERATION_FAILED: cage does not fit in the build volume"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Not all models fit within build volume"
        "404":
          description: A model in `model_ids` does not exist in the scene. Nothing is caged.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/label/:
    post:
      operationId: labelModels
      summary: Label models
      tags:
        - Modifying Models
      description: |
        Embosses or engraves a text label onto the surface of one or more
        models, at the given position and orientation. `position` and
        `orientation` are absolute build-platform coordinates, and the
        identical point and orientation are reused for every id in
        `model_ids`.

        Every model in `model_ids` must exist in the scene, and the label
        must apply cleanly to each one at the selected point. Otherwise the
        request fails and nothing is labeled.

        `depth_mm` is the height the label rises above the surface
        (`application_mode: "EMBOSS"`, the default) or the depth it is cut
        into the surface (`application_mode: "ENGRAVE"`).

        Labeling can fail for individual characters (e.g. an unsupported
        glyph) without failing the whole request. That is reported as a
        `warnings` entry rather than an error. If every character fails,
        the request itself fails with `OPERATION_FAILED`.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id`, `orientation`, `position`, `label`, `font_size_mm`, or `depth_mm` is missing or malformed. Also returned when `font_size_mm`/`depth_mm` is outside its allowed range, `application_mode` is outside its enum, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | A model in `model_ids` does not exist in the scene (nothing is labeled), or label generation itself failed, e.g. the shared `position`/`orientation` don't land on one of the selected models' surfaces. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LabelRequest'
            examples:
              Emboss:
                summary: Emboss a label on one model
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  label: "Form 4"
                  orientation: {x: 0.0, y: 0.0, z: 0.0}
                  position: {x: 0.0, y: 0.0, z: 5.0}
                  font_size_mm: 5
                  depth_mm: 0.5
              Engrave:
                summary: Engrave a label instead of embossing it
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  label: "SN-0042"
                  application_mode: "ENGRAVE"
                  orientation: {x: 0.0, y: 0.0, z: 0.0}
                  position: {x: 0.0, y: 0.0, z: 5.0}
                  font_size_mm: 4
                  depth_mm: 0.3
      responses:
        "200":
          description: >
            The label was applied to every selected model. `warnings` is
            non-empty if some characters of the label could not be created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LabelResult'
              examples:
                Success:
                  value:
                    warnings: []
                    infos: []
                PartialCharacterFailure:
                  summary: Some characters of the label could not be created
                  value:
                    warnings: ["Partial failure: 2 characters failed to be created"]
                    infos: []
        "202":
          description: >
            Labeling started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene or model, or labeling failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                ModelNotFound:
                  summary: "OPERATION_FAILED: unknown model id"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Model not found"
                LabelingFailed:
                  summary: >-
                    OPERATION_FAILED: no label component could be
                    created, e.g. because position/orientation missed the
                    model's surface
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Failed to create label for part: No label component was successfully created"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/drill/:
    post:
      operationId: drillHoles
      summary: Drill holes
      tags:
        - Modifying Models
      description: |
        Drills one or more drain/vent holes into the selected models at the
        given positions and orientations, so trapped resin can escape
        during printing. Set `create_plug: true` on a hole to also generate
        a matching plug model that can be reinserted after printing. Each
        created plug is reported in `plugs` with its own id and the id of
        the drilled model it was cut from.

        The same set of `drain_holes` is applied to every selected model.
        Every model must exist in the scene, or the request fails and
        nothing is drilled.

        Each hole's `position` and `orientation` are absolute
        build-platform coordinates, not positions relative to each model:
        the identical point and axis are reused for every id in
        `model_ids`. Selecting several models only drills a sensible hole
        on each of them when they actually share that location (e.g. exact
        duplicates stacked in place). Otherwise the point will miss the
        surface of most of the selected models.

        Problems with an individual hole (the given point is not on the
        model's surface, the hole is too large for the model, or a
        requested plug would be too small to print) do not fail the
        request. They are reported in `warnings`, identified by the hole's
        index into `drain_holes`, and that hole is skipped for that model.
        This makes an off-surface point far less disruptive here than for
        `/v1/scene/model/label/`: the other holes and models still proceed.
        If **nothing** was accomplished (every hole was skipped on every
        selected model and no plug was created), the request fails with
        `OPERATION_FAILED` instead, and the scene is left unmodified.

        Set `depth_mm` to `"AUTO"` to infer the hole depth by searching
        into the model along the hole's axis. `max_search_distance` bounds
        how far that search goes, and is only accepted together with
        `depth_mm: "AUTO"`.

        Drilling does not work on non-manifold models, but does not verify
        its input either, because that check is not free. If there is any doubt
        about a model's integrity, check it with
        `/v1/scene/model/validate/manifoldness/` first and repair it with
        `/v1/scene/model/repair/` if it is not manifold. Failure reports
        repeat this suggestion.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `drain_holes` is missing or empty, an entry is missing a required field or has the wrong type, an entry sets `max_search_distance` without `depth_mm: "AUTO"`, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID or no scene with that id exists, or a model in `model_ids` does not exist in the scene. Nothing is drilled. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | Nothing was accomplished: every hole was skipped on every selected model and no plug was created. The message carries the per-hole reasons and suggests the manifoldness-validation endpoint. The scene was not modified. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DrillRequest'
            examples:
              AutoDepth:
                summary: Drill a hole with an auto-inferred depth, no plug
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  drain_holes:
                    - orientation: {x: 0.0, y: 0.0, z: 0.0}
                      position: {x: 0.0, y: 0.0, z: 0.0}
                      diameter_mm: 3.0
                      depth_mm: "AUTO"
                      outset_mm: 1.0
              WithPlug:
                summary: Drill a fixed-depth hole and generate a plug for it
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  drain_holes:
                    - orientation: {x: 0.0, y: 0.0, z: 1.0}
                      position: {x: 2.67, y: 0.0, z: 0.33}
                      diameter_mm: 3.0
                      depth_mm: 4.0
                      outset_mm: 1.0
                      create_plug: true
      responses:
        "200":
          description: >
            The drain holes were processed. Always `200`, even when some or
            all requested holes could not be drilled. Check `warnings`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DrillResult'
              examples:
                Success:
                  value:
                    plugs: []
                    warnings: []
                    infos: []
                PlugCreated:
                  summary: create_plug was true for one hole
                  value:
                    plugs:
                      - plug_model_id: "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
                        drilled_model_id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                    warnings: []
                    infos: []
                HoleTooLarge:
                  summary: One hole could not be drilled
                  value:
                    plugs: []
                    warnings: ["Drain hole is too large for drain hole [0]"]
                    infos: []
        "202":
          description: >
            Drilling started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene or model, or a drain hole was misconfigured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                ModelNotFound:
                  summary: "INPUT_ERROR: unknown model id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Model ID is not in the scene"
                MaxSearchDistanceWithoutAuto:
                  summary: 'JSON_SCHEMA_MATCH_ERROR: max_search_distance requires depth_mm: "AUTO"'
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Failed to validate against schema associated with property name 'max_search_distance'. in reference to key path: /drain_holes/0"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/hollow/:
    post:
      operationId: hollowModels
      summary: Hollow models
      tags:
        - Modifying Models
      description: |
        Hollows the selected models, replacing each one's interior with a
        cavity that leaves a shell of `wall_thickness_mm` behind. If a
        model already has a hollow applied, this replaces it with the new
        parameters rather than hollowing the already-hollow shell again.

        `feature_size_mm` is the smallest feature size the generated inner
        surface will resolve. Smaller values increase quality at the cost
        of processing time. Values above 1 are clamped to 1.

        `accuracy` controls how closely hollowing follows the model's true
        offset surface, with speed as the tradeoff: 1 is the most accurate
        and slowest, 0 the fastest and coarsest. The default is 0.5.

        Hollowing can fail for an individual model without failing the whole
        request. This is most commonly because `wall_thickness_mm` is too
        large for its geometry. That model is left unhollowed, listed in
        `unhollowed_model_ids` and explained in `warnings`, while every
        other selected model still proceeds. If **no** selected model could
        be hollowed, the request fails with `OPERATION_FAILED` instead, and
        the scene is left unmodified.

        Hollowing does not work on non-manifold models, but does not verify
        its input either, because that check is not free. If there is any doubt
        about a model's integrity, check it with
        `/v1/scene/model/validate/manifoldness/` first and repair it with
        `/v1/scene/model/repair/` if it is not manifold. Failure reports
        repeat this suggestion.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `wall_thickness_mm` is missing or below its minimum, `feature_size_mm` or `accuracy` is outside its allowed range, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID or no scene with that id exists, or a model in `model_ids` does not exist in the scene. Nothing is hollowed. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | No selected model could be hollowed. The message carries the per-model reasons and suggests the manifoldness-validation endpoint. The scene was not modified. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HollowRequest'
            examples:
              AllModels:
                summary: Hollow every model in the scene with default quality settings
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  wall_thickness_mm: 1.5
              OneModelHighQuality:
                summary: Hollow a single model, favoring quality over speed
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  wall_thickness_mm: 1.5
                  feature_size_mm: 0.5
                  accuracy: 0.8
      responses:
        "200":
          description: >
            At least one selected model was hollowed. Reports which selected
            models were hollowed and which were left unhollowed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HollowResult'
              examples:
                Success:
                  value:
                    hollowed_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                    unhollowed_model_ids: []
                    warnings: []
                    infos: []
                WallTooThick:
                  summary: wall_thickness_mm was too large for one model's geometry
                  value:
                    hollowed_model_ids: ["9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"]
                    unhollowed_model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                    warnings:
                      - "Failed to hollow model with ID: c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                      - "If a model may not be manifold, you can check it with /v1/scene/model/validate/manifoldness/ and repair it with /v1/scene/model/repair/."
                    infos: []
        "202":
          description: >
            Hollowing started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, or referenced a missing scene or model.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                ModelNotFound:
                  summary: "INPUT_ERROR: unknown model id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Model ID is not in the scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/dental-hollow/:
    post:
      operationId: dentalHollowModels
      summary: Dental-hollow models
      tags:
        - Modifying Models
      description: |
        Cuts each selected model at a plane, extrudes a solid base of
        `extrude_distance_mm` downward from the cut to make it printable,
        and replaces the original model in the scene with the result. Set
        `hollow: true` to also hollow that result using dental-specific
        parameters (drain holes, honeycomb infill) rather than
        `/v1/scene/model/hollow/`'s.

        This is the API counterpart to Desktop PreForm's "Scan to Model"
        tool in the Dental Workspace sidebar: it uses similar
        orient/trim/extrude/hollow processing, but runs it on models
        already in the scene and replaces its manual cut-plane
        placement with automatic gumline detection.

        Designed and tested for dental arch models (e.g. intraoral or
        model scans of a full or partial arch). Behavior on other model
        types is unsupported.

        The cut plane is either an absolute height (`cutoff_height_mm`) or
        an offset from each model's automatically detected gumline
        (`cutoff_below_gumline_mm`, default `0`, exactly at the gumline).
        These two are mutually exclusive.

        `shell_thickness_mm`, `wall_thickness_mm`, `drain_hole_radius_mm`,
        `drain_hole_height_ratio`, `drain_hole_suppression_distance_mm`,
        `drain_hole_max_count`, and `enable_honeycomb_infill` only take
        effect when `hollow: true`. Supplying any of them while `hollow`
        is `false` (the default) is rejected. `wall_thickness_mm` here is the thickness of the
        honeycomb infill walls inside the hollow cavity, not the outer
        shell, which is `shell_thickness_mm`.

        Every model in `model_ids` must exist in the scene, or the request
        fails and nothing is changed. The response returns one `results`
        entry per selected model, in request order, naming the id of the
        processed model created from it. The source models themselves no
        longer exist in the scene.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` or `extrude_distance_mm` is missing, `cutoff_height_mm` and `cutoff_below_gumline_mm` are both given, `hollow` is `false` (the default) while any hollow-only field (`shell_thickness_mm`, `wall_thickness_mm`, `drain_hole_radius_mm`, `drain_hole_height_ratio`, `drain_hole_suppression_distance_mm`, `drain_hole_max_count`, `enable_honeycomb_infill`) is given, a field has the wrong type, a numeric field is outside its allowed range, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | A model in `model_ids` does not exist in the scene, or the cut/extrude/hollow processing itself failed. Nothing is changed. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DentalHollowingRequest'
            examples:
              AbsoluteCutoff:
                summary: Cut at an absolute height, without hollowing
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  cutoff_height_mm: 10
                  extrude_distance_mm: 2.1
                  hollow: false
              RelativeCutoffHollowed:
                summary: Cut relative to the detected gumline, then hollow the result
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
                  cutoff_below_gumline_mm: 3
                  extrude_distance_mm: 2.1
                  hollow: true
                  shell_thickness_mm: 2.75
                  drain_hole_suppression_distance_mm: 5
                  drain_hole_max_count: 10
      responses:
        "200":
          description: >
            The models were processed. `results` has one entry per selected
            model, in request order. The source models no longer exist in
            the scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DentalHollowingResult'
              examples:
                Hollowed:
                  summary: The selected model was cut, extruded, and hollowed
                  value:
                    results:
                      - source_model_id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                        new_model_ids: ["b7e2b0f4-9a2d-4b3d-8f4a-2e7c1a9d10c2"]
                    warnings: []
                    infos: []
        "202":
          description: >
            Processing started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, or referenced a missing scene or model.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                MutuallyExclusiveCutoffs:
                  summary: "JSON_SCHEMA_MATCH_ERROR: both cutoff fields given"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Failed to validate against schema associated with property name 'cutoff_below_gumline_mm'."
                ModelNotFound:
                  summary: "OPERATION_FAILED: unknown model id"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Model could not be found."
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/repair/:
    post:
      operationId: repairModels
      summary: Repair models
      tags:
        - Modifying Models
      description: |
        Attempt to repair the selected models' meshes: stitch open
        boundaries, split non-manifold vertices, remove degenerate faces,
        and fill holes. Each repaired model keeps its id, position,
        orientation, and metadata. Only its mesh is replaced.

        Repair is **best-effort**: a `200` response means the repair pipeline
        ran to completion for every selected model, and each model's entry
        reports the manifold library's verdict on the repaired mesh, the
        same check as `/v1/scene/model/validate/manifoldness/`. A model
        whose entry says `is_manifold: false` still carries residual
        defects.

        The scene is committed atomically: if repairing any selected model
        fails, no model's mesh is replaced.

        Repair is a slow operation: its worst-case runtime is O(n²) in the
        number of triangles in the mesh. Prefer `?async=true` and poll the
        returned operation. `/v1/scene/model/validate/manifoldness/` is
        much cheaper (though still not free). When you only suspect a
        model may be broken, check first and repair only the models that
        are not manifold.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body does not match the request schema: `scene_id` missing, a field has the wrong type, `model_ids` is present but not an array of strings, or an unknown field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | The repair itself failed for one of the models, or mesh repair is not available in this build. The message carries the underlying reason. The scene was not modified. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. The scene was not modified. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RepairModelsRequest'
            examples:
              RepairWholeScene:
                summary: Repair every model in the scene
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              RepairOneModel:
                summary: Repair a single model
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
      responses:
        "200":
          description: >
            Every selected model was repaired and the scene was updated. One
            entry per selected model, keyed by model id, reporting the
            manifold library's verdict on the repaired mesh.
          content:
            application/json:
              schema:
                type: object
                required: [per_model_results]
                properties:
                  per_model_results:
                    type: object
                    description: Map of model id to that model's repair result.
                    additionalProperties:
                      $ref: '#/components/schemas/ModelRepairResult'
              examples:
                OneFullOnePartial:
                  summary: One model fully repaired, one with residual defects
                  value:
                    per_model_results:
                      "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10": {is_manifold: true}
                      "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55": {is_manifold: false}
        "202":
          description: >
            The repair started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: >
            The request was malformed, referenced a missing scene, or the
            repair itself failed. On failure the scene is left unmodified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                UnknownField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: undeclared field present"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "additional properties are not permitted in this context in reference to key path: /"
                RepairUnavailable:
                  summary: "OPERATION_FAILED: mesh repair not available in this build"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Mesh repair is not available in this build"
        "404":
          description: >
            A model in `model_ids` does not exist in the scene. No models
            were modified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/split-bodies/:
    post:
      operationId: splitModelBodies
      summary: Split models into separate bodies
      tags:
        - Modifying Models
      description: |
        Splits each selected model into one new model per disconnected body
        in its mesh, removing the original. Each new model is named
        `"<original name> (Body N)"` and keeps the original's position and
        orientation, subject to the printer's default build-platform snap
        settings (SLA printers snap each new body to the platform
        in Z, while SLS does not). Supports are not carried over to the new
        models.

        Returns one `results` entry per selected model, in request order,
        naming the ids of the bodies split out of it (in no particular
        order. Each body's name carries its `(Body N)` index). The source
        models themselves no longer exist in the scene.

        Models are validated before any are split: if any requested model
        does not exist, nothing is split. Splitting is also all-or-nothing
        across the whole request: if any selected model has only one
        connected body (or no valid geometry) and cannot be split, no model
        in the request is split.

        Use `/v1/scene/model/body-count/` first to find which models have
        more than one body.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. Nothing is split. |
        | 400 | `OPERATION_FAILED` | A model in `model_ids` has only one connected body (or no valid geometry) and cannot be split. Nothing is split. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SplitModelBodiesRequest'
            examples:
              SpecificModels:
                summary: Split two specific models
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids:
                    - "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                    - "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
              AllModels:
                summary: Split every multi-body model in the scene
                description: Omitting model_ids selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: >
            The selected models were split. `results` has one entry per
            selected model, in request order. The source models no longer
            exist in the scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SplitModelBodiesResult'
              examples:
                TwoBodies:
                  summary: One selected model split into two bodies
                  value:
                    results:
                      - source_model_id: "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                        new_model_ids:
                          - "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
                          - "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"
        "400":
          description: The request was malformed, referenced a missing scene, or splitting failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                InvalidModelIds:
                  summary: "JSON_SCHEMA_MATCH_ERROR: model_ids is not an array"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Value type not permitted by 'type' constraint. in reference to key path: /model_ids"
                SceneNotFound:
                  summary: "INPUT_ERROR: missing or unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                SingleBodyModel:
                  summary: "OPERATION_FAILED: a selected model cannot be split"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 has only one body and cannot be split"
        "404":
          description: A model in `model_ids` does not exist in the scene. Nothing is split.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/body-count/:
    post:
      operationId: getModelBodyCounts
      summary: Get model body counts
      tags:
        - Modifying Models
      description: |
        Returns the number of disconnected bodies in each selected model's
        mesh. A `body_count` greater than 1 means the model consists of
        several disconnected shells, which is often unintentional, e.g.
        several parts imported together as one file.
        Use `/v1/scene/model/split-bodies/` to split such a model into one
        model per body.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetModelBodyCountRequest'
            examples:
              SpecificModels:
                summary: Get the body count of two specific models
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids:
                    - "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"
                    - "a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7"
              AllModels:
                summary: Get the body count of every model in the scene
                description: Omitting model_ids selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: >
            One entry per requested model, keyed by model id. If `model_ids`
            is omitted or empty, every model in the scene is counted.
          content:
            application/json:
              schema:
                type: object
                required: [per_model_results]
                properties:
                  per_model_results:
                    type: object
                    description: Map of model id to that model's body count.
                    additionalProperties:
                      $ref: '#/components/schemas/BodyCountResult'
              examples:
                MultiBodyModel:
                  summary: One multi-body model and one ordinary model
                  value:
                    per_model_results:
                      "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10": {body_count: 2}
                      "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55": {body_count: 1}
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                InvalidModelIds:
                  summary: "JSON_SCHEMA_MATCH_ERROR: model_ids is not an array"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Value type not permitted by 'type' constraint. in reference to key path: /model_ids"
                SceneNotFound:
                  summary: "INPUT_ERROR: missing or unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        "404":
          description: A model in `model_ids` does not exist in the scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/validate/cups/:
    post:
      operationId: validateCups
      summary: Detect cups
      tags:
        - Printability Checks
      description: |
        Analyze every printable model in the scene for *cups*: cavities that
        face the build platform and seal against the resin surface as the
        part prints. Cups trap air or resin, causing suction forces during
        peel that lead to print failure, and trapped uncured resin in the
        finished part.

        The check runs per model and returns the number of cups found in
        each. A `cup_count` of `0` means the model's current orientation
        produces no cups. To fix a model with cups, reorient it (manually or
        with `/v1/scene/model/auto-orient/`) or add drain holes
        (`/v1/scene/model/drill/`).

        The whole scene is always analyzed. There is no model selection for
        this endpoint. Detection can take tens of seconds on complex scenes,
        so prefer `?async=true` and poll the returned operation.

        This endpoint reads the scene without modifying it. It is a POST
        because its input travels in the request body. The body carries only
        the scene reference.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `MISSING_PARAMETER` | The body has no `scene_id` field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | The detection task itself failed. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        description: Identifies the scene to analyze.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene to analyze.
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: >
            Detection completed. One entry per printable model in the scene,
            keyed by model id. An empty `per_model_results` means the scene
            contains no printable models.
          content:
            application/json:
              schema:
                type: object
                required: [per_model_results]
                properties:
                  per_model_results:
                    type: object
                    description: Map of model id to that model's cup-detection result.
                    additionalProperties:
                      $ref: '#/components/schemas/CupDetectionResult'
              examples:
                OneCleanOneCupped:
                  summary: Two models, one with cups
                  description: >
                    The first model prints without suction problems. The
                    second has two cup cavities in its current orientation.
                  value:
                    per_model_results:
                      "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10": {cup_count: 0}
                      "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55": {cup_count: 2}
                EmptyScene:
                  summary: Scene with no printable models
                  value:
                    per_model_results: {}
        "202":
          description: >
            Detection started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "MISSING_PARAMETER: no scene_id in the body"
                  value:
                    error:
                      code: "MISSING_PARAMETER"
                      message: "scene_id is required"
                InvalidSceneId:
                  summary: "INPUT_ERROR: scene_id is not a UUID"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Invalid scene_id format"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
                DetectionFailed:
                  summary: "OPERATION_FAILED: the analysis itself failed"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Cup detection failed"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/validate/minima/:
    post:
      operationId: validateMinima
      summary: Detect unsupported minima
      tags:
        - Printability Checks
      description: |
        Analyze every printable model in the scene for *unsupported
        minima*: local low points on the surface that the model's current
        supports don't reach. Unsupported minima may break off the part
        during printing and lead to print failures.

        The check runs per model and returns the number of unsupported
        minima found in each. A `minima_count` of `0` means every low
        point on the model is currently supported. To fix a model with
        minima, add supports manually or regenerate them
        (`/v1/scene/model/auto-support/`).

        The whole scene is always analyzed. There is no model selection
        for this endpoint. Detection can take tens of seconds on complex
        scenes, so prefer `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `MISSING_PARAMETER` | The body has no `scene_id` field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | The detection task itself failed. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        description: Identifies the scene to analyze.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene to analyze.
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: >
            Detection completed. One entry per printable model in the scene,
            keyed by model id. An empty `per_model_results` means the scene
            contains no printable models.
          content:
            application/json:
              schema:
                type: object
                required: [per_model_results]
                properties:
                  per_model_results:
                    type: object
                    description: Map of model id to that model's minima-detection result.
                    additionalProperties:
                      $ref: '#/components/schemas/MinimaDetectionResult'
              examples:
                OneCleanOneWithMinima:
                  summary: Two models, one with unsupported minima
                  description: >
                    The first model is fully supported. The second has
                    three unsupported local minima.
                  value:
                    per_model_results:
                      "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10": {minima_count: 0}
                      "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55": {minima_count: 3}
                EmptyScene:
                  summary: Scene with no printable models
                  value:
                    per_model_results: {}
        "202":
          description: >
            Detection started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "MISSING_PARAMETER: no scene_id in the body"
                  value:
                    error:
                      code: "MISSING_PARAMETER"
                      message: "scene_id is required"
                InvalidSceneId:
                  summary: "INPUT_ERROR: scene_id is not a UUID"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Invalid scene_id format"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
                DetectionFailed:
                  summary: "OPERATION_FAILED: the analysis itself failed"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Minima detection failed"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/validate/supportedness/:
    post:
      operationId: validateSupportedness
      summary: Detect undersupported area
      tags:
        - Printability Checks
      description: |
        Analyze every printable model in the scene for how much of its
        surface is undersupported. This is the same calculation behind
        the desktop app's "Undersupported Models" red-shading toggle.

        The check runs per model and returns `percent_unsupported`, on a
        0-100 scale. `0` means the model is fully supported in its
        current orientation and support configuration. Higher values
        mean more of the surface is at risk of printing incompletely or
        breaking off during the print.

        The whole scene is always analyzed. There is no model selection
        for this endpoint. Detection can take tens of seconds on complex
        scenes, so prefer `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `MISSING_PARAMETER` | The body has no `scene_id` field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | The detection task itself failed. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        description: Identifies the scene to analyze.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene to analyze.
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: >
            Detection completed. One entry per printable model in the scene,
            keyed by model id. An empty `per_model_results` means the scene
            contains no printable models.
          content:
            application/json:
              schema:
                type: object
                required: [per_model_results]
                properties:
                  per_model_results:
                    type: object
                    description: Map of model id to that model's supportedness-detection result.
                    additionalProperties:
                      $ref: '#/components/schemas/SupportednessDetectionResult'
              examples:
                OneFullyOnePartiallySupported:
                  summary: Two models, one fully supported, one partially
                  value:
                    per_model_results:
                      "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10": {percent_unsupported: 0.0}
                      "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55": {percent_unsupported: 42.5}
                EmptyScene:
                  summary: Scene with no printable models
                  value:
                    per_model_results: {}
        "202":
          description: >
            Detection started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "MISSING_PARAMETER: no scene_id in the body"
                  value:
                    error:
                      code: "MISSING_PARAMETER"
                      message: "scene_id is required"
                InvalidSceneId:
                  summary: "INPUT_ERROR: scene_id is not a UUID"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Invalid scene_id format"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
                DetectionFailed:
                  summary: "OPERATION_FAILED: the analysis itself failed"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Supportedness detection failed"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/validate/thin-features/:
    post:
      operationId: validateThinFeatures
      summary: Detect thin walls
      tags:
        - Printability Checks
      description: |
        Analyze the selected models for walls thinner than
        `threshold_mm`: regions likely to be fragile, warp, or fail to
        cure fully during printing.

        Unlike the other printability-check endpoints, this one takes an
        explicit model selection (`model_ids`) and a required threshold,
        rather than always scanning every printable model in the scene.
        Omitting `model_ids` (or passing an empty array) analyzes every
        model currently in the scene, including non-printable ones.

        The check runs per model and returns an aggregate summary
        (`has_thin_walls`, `thin_volume_mm3`, `thin_region_count`) plus,
        when thin walls were found, a `thin_regions` breakdown of each
        connected thin region's volume and bounding box.

        Detection can take a while on complex scenes or a large model
        selection, so prefer `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `threshold_mm` is missing or not a positive number, `model_ids` is present but not an array of strings, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | The detection task failed, including when `model_ids` names an id that does not exist in the scene. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThinWallDetectionRequest'
            examples:
              AllModels:
                summary: Analyze every model in the scene with a 0.5 mm threshold
                description: Omitting `model_ids` selects all models.
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  threshold_mm: 0.5
              SpecificModels:
                summary: Analyze two specific models
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10", "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"]
                  threshold_mm: 0.6
      responses:
        "200":
          description: >
            Detection completed. One entry per analyzed model, keyed by
            model id. An empty `per_model_results` means the scene (or the
            requested `model_ids`) contains no models.
          content:
            application/json:
              schema:
                type: object
                required: [per_model_results]
                properties:
                  per_model_results:
                    type: object
                    description: Map of model id to that model's thin-wall-detection result.
                    additionalProperties:
                      $ref: '#/components/schemas/ThinWallDetectionResult'
              examples:
                OneCleanOneThin:
                  summary: One model with no thin regions, one with two
                  value:
                    per_model_results:
                      "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10":
                        status: ANALYZED
                        has_thin_walls: false
                        thin_volume_mm3: 0.0
                        thin_region_count: 0
                        thin_regions: []
                      "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55":
                        status: ANALYZED
                        has_thin_walls: true
                        thin_volume_mm3: 12.4
                        thin_region_count: 2
                        thin_regions:
                          - volume_mm3: 9.1
                            bounding_box:
                              min_corner: {x: -10.0, y: -5.0, z: 0.0}
                              max_corner: {x: -6.0, y: 5.0, z: 3.0}
                          - volume_mm3: 3.3
                            bounding_box:
                              min_corner: {x: 4.0, y: -2.0, z: 8.0}
                              max_corner: {x: 7.0, y: 2.0, z: 10.0}
        "202":
          description: >
            Detection started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the detection itself failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
                MissingThreshold:
                  summary: "JSON_SCHEMA_MATCH_ERROR: threshold_mm is missing"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'threshold_mm'."
                NonPositiveThreshold:
                  summary: "JSON_SCHEMA_MATCH_ERROR: threshold_mm is not positive"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "instance is not greater than 0 in reference to key path: /threshold_mm"
                UnrecognizedField:
                  summary: "JSON_SCHEMA_MATCH_ERROR: unrecognized field present"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Object contains a property that could not be validated using 'properties' or 'additionalProperties' constraints: 'models'."
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                InvalidSceneId:
                  summary: "INPUT_ERROR: scene_id is not a UUID"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Invalid scene_id format"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                ModelNotFound:
                  summary: "OPERATION_FAILED: a model in model_ids does not exist in the scene"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Model 9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/validate/interferences/:
    post:
      operationId: validateInterferences
      summary: Detect colliding models
      tags:
        - Printability Checks
      description: |
        Find pairs of models in the scene that overlap by more than the
        collision-detection offset. Models scaled, moved, or reoriented
        without re-validating the layout (see `/v1/scene/model/scale/`,
        `/v1/scene/model/translate/`, and `/v1/scene/model/orient/`, each
        of which recommends calling this endpoint afterwards) may end up
        overlapping without any other feedback.

        Every visible, printable model in the scene is considered. There
        is no model selection for this endpoint.

        `collision_offset_mm`, an optional body field, overrides the
        scene's own configured visual collision offset for this call
        only. If it is absent, the scene's configured offset is used.

        Unlike the other printability-check endpoints, this one does not
        support `?async=true`: colliding-models detection always runs
        synchronously.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` is missing, `collision_offset_mm` is not a number, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | The detection task itself failed. The message carries the underlying reason. |
      requestBody:
        required: true
        description: Identifies the scene to analyze, and optionally overrides the collision offset.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              additionalProperties: false
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene to analyze.
                collision_offset_mm:
                  type: number
                  description: >
                    Minimum gap, in millimeters, models must keep to not
                    be reported as colliding. Negative values allow a
                    controlled amount of overlap. Defaults to the
                    scene's own configured visual collision offset.
            examples:
              Default:
                summary: Use the default collision offset
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              WithOffset:
                summary: Override the collision offset for this call
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  collision_offset_mm: 0.5
      responses:
        "200":
          description: >
            Detection completed. Empty `model_ids` means no colliding
            pairs were found.
          content:
            application/json:
              schema:
                type: object
                required: [model_ids]
                properties:
                  model_ids:
                    type: array
                    description: >
                      Each entry is one colliding pair: a 2-element array
                      of the two models' ids. A model can appear in more
                      than one pair.
                    items:
                      type: array
                      minItems: 2
                      maxItems: 2
                      items:
                        type: string
                        format: uuid
              examples:
                OnePairColliding:
                  summary: Two models overlap and a third does not touch either
                  value:
                    model_ids:
                      - ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10", "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55"]
                NoCollisions:
                  summary: No models overlap
                  value:
                    model_ids: []
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                InvalidSceneId:
                  summary: "INPUT_ERROR: scene_id is not a UUID"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Invalid scene_id format"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/validate/manifoldness/:
    post:
      operationId: validateManifoldness
      summary: Check models are manifold
      tags:
        - Printability Checks
      description: |
        Validate that the selected models are *manifold solids*: closed,
        watertight meshes where every edge is shared by exactly two
        triangles. Non-manifold geometry (holes, open boundaries, bowtie
        vertices, self-inconsistent triangulation) can slice incorrectly
        and cause print failures.

        The check runs per model using the [manifold](https://github.com/elalish/manifold)
        library and reports each model's verdict. A model that is not
        manifold can be repaired with `/v1/scene/model/repair/`.

        Some operations require manifold input: `/v1/scene/model/hollow/`
        and `/v1/scene/model/drill/` do not work on non-manifold models
        (`/v1/scene/model/dental-hollow/` is unaffected because it rebuilds
        the surface rather than operating on the input topology). Run this
        check, and repair if it fails, before relying on those
        endpoints.

        Omitting `model_ids` checks every model in the scene. This endpoint
        reads the scene without modifying it, and is a POST only because
        its input travels in the request body.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body does not match the request schema: `scene_id` missing, a field has the wrong type, `model_ids` is present but not an array of strings, or an unknown field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `OPERATION_FAILED` | The check itself failed. The message carries the underlying reason. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckManifoldRequest'
            examples:
              WholeScene:
                summary: Check every model in the scene
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              OneModel:
                summary: Check a single model
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
      responses:
        "200":
          description: >
            The check completed. One entry per selected model, keyed by
            model id. An empty `per_model_results` means the selection
            matched no models.
          content:
            application/json:
              schema:
                type: object
                required: [per_model_results]
                properties:
                  per_model_results:
                    type: object
                    description: Map of model id to that model's manifold-check result.
                    additionalProperties:
                      $ref: '#/components/schemas/ManifoldCheckResult'
              examples:
                OneCleanOneBroken:
                  summary: Two models, one not manifold
                  value:
                    per_model_results:
                      "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10": {is_manifold: true, status: "MANIFOLD"}
                      "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55": {is_manifold: false, status: "NOT_MANIFOLD"}
        "202":
          description: >
            The check started asynchronously (`?async=true`). Poll the
            operations endpoints with the returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'scene_id' not found in reference to key path: /"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        "404":
          description: >
            A model in `model_ids` does not exist in the scene. No models
            were checked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/model/validate/in-bounds/:
    post:
      operationId: validateInBounds
      summary: Check models fit in the build volume
      tags:
        - Printability Checks
      description: |
        Report whether each selected model, **at its current position,
        orientation and scale**, fits inside the build volume of the
        scene's printer type.

        This is a direct read of scene state, not a computation: it never
        reorients, rescales, moves or re-lays-out anything, and it does not
        report whether a model *could* fit in some other pose. A model that
        would fit if rotated is still reported `in_bounds: false` while it
        sticks out where it currently sits.

        The verdict includes a model's supports, so adding supports can take
        an in-bounds model out of bounds. It is the same verdict as the
        `in_bounds` field of `/v1/scene/model/list/`; this endpoint exists so
        that callers whose only question is "does it fit?" can ask it
        directly rather than reading and filtering full model state.

        Models out of the build volume are an ordinary, expected result
        here, reported as `in_bounds: false` with a `200`. Unlike
        `/v1/scene/model/validate/cups/` and
        `/v1/scene/model/validate/minima/`, this endpoint does not fail with
        `OPERATION_FAILED` when a model does not fit.

        Omitting `model_ids` checks every model in the scene. This endpoint
        reads the scene without modifying it, and is a POST only because its
        input travels in the request body. The check is immediate, so there
        is no `async` form.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body does not match the request schema: `scene_id` missing, a field has the wrong type, `model_ids` is present but not an array of strings, or an unknown field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 404 | `NOT_FOUND` | A model in `model_ids` does not exist in the scene. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InBoundsDetectionRequest'
            examples:
              WholeScene:
                summary: Check every model in the scene
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
              OneModel:
                summary: Check a single model
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  model_ids: ["c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10"]
      responses:
        "200":
          description: >
            The check completed. One entry per selected model, keyed by
            model id. An empty `per_model_results` means the selection
            matched no models, in which case `all_in_bounds` is `true`.
          content:
            application/json:
              schema:
                type: object
                required: [per_model_results, all_in_bounds]
                properties:
                  per_model_results:
                    type: object
                    description: Map of model id to that model's build-volume verdict.
                    additionalProperties:
                      $ref: '#/components/schemas/InBoundsResult'
                  all_in_bounds:
                    type: boolean
                    description: >
                      Whether every selected model is in bounds. `true` when
                      `per_model_results` is empty.
              examples:
                OneInOneOut:
                  summary: Two models, one sticking out of the build volume
                  value:
                    per_model_results:
                      "c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10": {in_bounds: true}
                      "9b1dd8a0-4f6e-45c2-9a3e-77a01f2b6c55": {in_bounds: false}
                    all_in_bounds: false
        "400":
          description: The request was malformed or referenced a missing scene.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "required property 'scene_id' not found in reference to key path: /"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        "404":
          description: >
            A model in `model_ids` does not exist in the scene. No models
            were checked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ModelNotFound:
                  summary: "NOT_FOUND: unknown model id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Model c2a37eb2-8e4b-4b8e-9f37-1f5f2c1a9d10 not found in scene"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/print/:
    post:
      operationId: printScene
      summary: Upload a scene's sliced job to a printer
      tags:
        - Scenes
      description: |
        Runs the full slicing pipeline for a scene and uploads the
        resulting job to `printer`, identified by serial name, IP
        address, or Fleet Control Queue id. This does not modify the scene.

        `print_now` controls whether the job prints immediately or is
        queued: omitting it defers to the printer's current
        `ready_to_print_now` state. Requesting `print_now: true` on a
        printer that cannot start immediately (e.g. a Cloud Queue) fails
        with `OPERATION_FAILED`.

        This can take a while, especially finding the printer on the
        network, so prefer `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id`, `printer`, or `job_name` is missing or not a string, `find_printer_timeout_seconds` is present but not a non-negative integer, `print_now` is present but not a boolean, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | The printer could not be found within `find_printer_timeout_seconds`, slicing failed, or the job could not be uploaded (e.g. `print_now: true` on a printer that cannot print immediately). The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrintSceneRequest'
            examples:
              Default:
                summary: Upload the job to a printer by name
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  printer: "ArtfulChameleon"
                  job_name: "My Job"
              PrintNow:
                summary: Require the job to print immediately
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  printer: "192.168.1.42"
                  job_name: "My Job"
                  print_now: true
      responses:
        "200":
          description: The job was uploaded to the printer.
          content:
            application/json:
              schema:
                type: object
                required: [job_id]
                properties:
                  job_id:
                    type: string
                    format: uuid
                    description: The id of the newly created print job.
              examples:
                Uploaded:
                  value:
                    job_id: "9c858901-8a57-4791-81fe-4c455b099bc9"
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, referenced a missing scene, or the printer/job could not be found or uploaded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                PrinterNotFound:
                  summary: "OPERATION_FAILED: the printer could not be found"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Couldn't find printer, timeout occurred."
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/scene/metadata/:
    post:
      operationId: getSceneMetadata
      summary: Get a scene's print-job metadata
      tags:
        - Scenes
      description: |
        Returns the job-metadata JSON document PreForm embeds in print jobs
        for this scene: the subset that can be computed from the scene's
        current state without slicing. The document is a passthrough of the
        printer job-metadata format: keys are PascalCase, the exact shape
        depends on the scene's printer family, and fields beyond the
        example will appear. This does not modify the scene. It is a `POST`
        (rather than `GET`) only because the request includes a JSON body
        to identify the scene.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `MISSING_PARAMETER` | The body has no `scene_id` field. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | The metadata could not be computed (e.g. the scene was deleted while the request was in flight). |
      requestBody:
        required: true
        description: Identifies the scene to compute job metadata for.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id]
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene.
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        "200":
          description: The scene's print-job metadata.
          content:
            application/json:
              schema:
                type: object
                description: >
                  The printer job-metadata document. Its shape depends on
                  the scene's printer family. Treat it as an open object.
              examples:
                Truncated:
                  summary: "A Form 4 scene (truncated: real responses carry many more fields)"
                  value:
                    MaterialCode: "FLGPGR05"
                    AllowedMachineTypeIds: ["FORM-4-0"]
        "400":
          description: The request was malformed, referenced a missing scene, or the metadata could not be computed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingSceneId:
                  summary: "MISSING_PARAMETER: no scene_id in the body"
                  value:
                    error:
                      code: "MISSING_PARAMETER"
                      message: "scene_id is required"
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/print-setting/list/:
    post:
      operationId: listPrintSettings
      summary: List print settings
      tags:
        - Print Settings
      description: |
        List every machine/material/layer-thickness/print-setting
        combination Formlabs ships settings for, grouped by printer type and
        material.

        Each material setting's `scene_settings` is the same shape accepted
        by `/v1/print-setting/validate/`, so a value from this list can be
        posted there directly to double-check it.

        Pass `compatible_with` in the request body to restrict the response
        to a single connected printer's type instead of the full catalog.
        Omitting it, or sending no body at all, lists the full catalog.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body has an unknown field, or `compatible_with` isn't a string. |
        | 404 | `NOT_FOUND` | `compatible_with` was given but no connected printer has that serial name. |
        | 500 | `INTERNAL_ERROR` | Server misconfiguration: a printer type PreForm ships settings for is not registered. Not triggerable by request input. |
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrintSettingListRequest'
            examples:
              Catalog:
                summary: No/empty body to get the full catalog
                value: {}
              CompatibleWith:
                summary: Restrict to one connected printer
                value:
                  compatible_with: "SN123456"
      responses:
        "200":
          description: >
            The catalog of supported combinations, or (if `compatible_with`
            matched) the single connected printer's type.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrintSettingsListResponse'
              examples:
                Catalog:
                  summary: Excerpt of the full catalog
                  value:
                    printer_types:
                      - label: "Form 4"
                        supported_machine_type_ids: ["FORM-4-0"]
                        supported_product_names: ["Form 4"]
                        build_volume_dimensions_mm: [200, 125, 210]
                        materials:
                          - label: "Black V5"
                            description: "Fine detail, matte black finish"
                            material_settings:
                              - label: "0.025 mm (Default settings)"
                                scene_settings:
                                  machine_type: "FORM-4-0"
                                  material_code: "FLGPBK05"
                                  print_setting: "DEFAULT"
                                  layer_thickness_mm: 0.025
                      - label: "Fuse 1"
                        supported_machine_type_ids: ["PILK-1-0"]
                        supported_product_names: ["Fuse 1"]
                        build_volume_dimensions_mm: [165, 165, 300]
                        materials:
                          - label: "Nylon 11 V1"
                            description: "Strong, ductile material with high impact-resistance"
                            material_settings:
                              - label: "0.110 mm (Default settings)"
                                scene_settings:
                                  machine_type: "PILK-1-0"
                                  material_code: "FLP11B01"
                                  print_setting: "DEFAULT"
                                  layer_thickness_mm: 0.11
                      - label: "Fuse X1"
                        supported_machine_type_ids: ["FUSX-1-0", "FUSL-2-0"]
                        supported_product_names: ["Fuse X1"]
                        build_volume_dimensions_mm: [330, 330, 565]
                        materials:
                          - label: "Nylon 12 V1"
                            description: "Strong, durable material with high environmental stability"
                            material_settings:
                              - label: "0.110 mm (Default settings)"
                                scene_settings:
                                  machine_type: "FUSX-1-0"
                                  material_code: "FLP12G01"
                                  print_setting: "DEFAULT"
                                  layer_thickness_mm: 0.11
                CompatibleWith:
                  summary: compatible_with matched one connected Fuse 1+
                  value:
                    printer_types:
                      - label: "Fuse 1+"
                        supported_machine_type_ids: ["FS30-1-0"]
                        supported_product_names: ["Fuse 1+"]
                        build_volume_dimensions_mm: [165, 165, 300]
                        materials:
                          - label: "Polypropylene V1"
                            description: "Ductile, lightweight, and chemically resistant"
                            material_settings:
                              - label: "0.110 mm (Default settings)"
                                scene_settings:
                                  machine_type: "FS30-1-0"
                                  material_code: "FLPLPG01"
                                  print_setting: "DEFAULT"
                                  layer_thickness_mm: 0.11
        "404":
          description: A connected printer matching compatible_with was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                DeviceNotFound:
                  summary: "NOT_FOUND: no connected printer with that serial name"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Device with given ID not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/print-setting/validate/:
    post:
      operationId: validatePrintSetting
      summary: Validate a print setting combination
      tags:
        - Print Settings
      description: |
        Check whether a machine_type/material_code/print_setting/layer_thickness_mm
        combination is one Formlabs ships, without creating a scene.

        At least 3 of the 4 fields must be given. If exactly one is omitted,
        the response's `options` reports the valid values for that field
        given the other three. Omitting two or more is rejected, since each
        field's options are collected independently and nothing guarantees
        that combining one value from each list yields an actual valid
        combination.

        A well-formed but unrecognized `machine_type`, `material_code`,
        `print_setting`, or `layer_thickness_mm` produces a normal `200`
        response with `"valid": false`.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | The body doesn't match the request schema: fewer than 3 of the 4 fields present, an unknown field, a field with the wrong type, or `layer_thickness_mm` not a number/`"ADAPTIVE"`/numeric string. |
        | 400 | `INPUT_ERROR` | `machine_type` was given but is (or canonicalizes to) the reserved `UNKN-0-0` id. Not the same as an unrecognized machine type string, which yields `valid: false`. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrintSettingValidateRequest'
            examples:
              FullySpecifiedValid:
                summary: A fully-specified, valid combination
                value:
                  machine_type: "FORM-4-0"
                  material_code: "FLGPBK05"
                  print_setting: "DEFAULT"
                  layer_thickness_mm: 0.025
              FullySpecifiedInvalid:
                summary: A fully-specified combination that doesn't exist
                description: >
                  Black V5 on Form 4 exists but can't be printed at this thickness.
                value:
                  machine_type: "FORM-4-0"
                  material_code: "FLGPBK05"
                  print_setting: "DEFAULT"
                  layer_thickness_mm: 0.999
              PrintSettingOmitted:
                summary: One field omitted
                description: >
                  Omitting print_setting asks which print setting codes are
                  valid for this machine/material/thickness.
                value:
                  machine_type: "FORM-4-0"
                  material_code: "FLGPBK05"
                  layer_thickness_mm: 0.025
      responses:
        "200":
          description: The combination was checked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrintSettingValidateResponse'
              examples:
                FullySpecifiedValid:
                  summary: The fully-specified combination is valid
                  value:
                    valid: true
                FullySpecifiedInvalid:
                  summary: The fully-specified combination doesn't exist
                  value:
                    valid: false
                PrintSettingOmitted:
                  summary: Valid, with options for the omitted print_setting
                  value:
                    valid: true
                    options:
                      print_settings: ["DEFAULT"]
        "400":
          description: The request was malformed, or the combination given couldn't be interpreted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Error parsing request JSON"
                TooFewFields:
                  summary: "JSON_SCHEMA_MATCH_ERROR: fewer than 3 fields given"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "instance failed minProperties constraint in reference to key path: /"
                InvalidMachineType:
                  summary: "INPUT_ERROR: machine_type is the reserved unknown id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Invalid machine_type input"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/print-setting/save/:
    post:
      operationId: savePrintSetting
      summary: Save a scene's print setting to a .fps file
      tags:
        - Print Settings
      description: |
        Exports the scene's current machine/material/print-setting/
        layer-thickness combination, including any manual field overrides
        made in that scene, to a reusable `.fps` print settings file.

        The saved file can later be applied to another scene via the
        `fps_file` field of `/v1/scene/create/` or `/v1/scene/update/`.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `scene_id` or `file` is missing, `file` is empty or not a string, or an unrecognized field is present. |
        | 400 | `INPUT_ERROR` | `scene_id` is not a well-formed UUID, or no scene with that id exists. |
        | 400 | `OPERATION_FAILED` | `file`'s extension is not `.fps`, or the file could not be written. The message carries the underlying reason. |
      requestBody:
        required: true
        description: Identifies the scene to export from and the destination file.
        content:
          application/json:
            schema:
              type: object
              required: [scene_id, file]
              additionalProperties: false
              properties:
                scene_id:
                  type: string
                  format: uuid
                  description: The id of the scene whose print setting to save.
                file:
                  type: string
                  minLength: 1
                  description: Path to write the .fps file to.
            examples:
              Default:
                value:
                  scene_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  file: "C:\\Users\\user\\Desktop\\custom.fps"
      responses:
        "200":
          description: The .fps file was saved. The response has no body.
        "400":
          description: The request was malformed, referenced a missing scene, or the file could not be saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not valid JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Invalid JSON input"
                MissingFile:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no file in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'file'."
                EmptyFile:
                  summary: "JSON_SCHEMA_MATCH_ERROR: file is an empty string"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "String should be no fewer than 1 characters in length. in reference to key path: /file"
                MissingSceneId:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no scene_id in the body"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'scene_id'."
                SceneNotFound:
                  summary: "INPUT_ERROR: unknown scene_id"
                  value:
                    error:
                      code: "INPUT_ERROR"
                      message: "Scene with given scene_id not found"
                UnsupportedExtension:
                  summary: "OPERATION_FAILED: file's extension is not .fps"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Unexpected filetype: txt"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/operation/get/:
    get:
      operationId: getOperation
      summary: Get one operation's status and result
      tags:
        - Operations
      description: |
        Returns the full record of one operation started by a `?async=true`
        request: its status, its progress, and, once `status` is `SUCCEEDED`
        or `FAILED`, the `result` the originating endpoint would have
        returned synchronously. This is the URL returned in `operation_url`
        (and the `Location` header) of every `202 Accepted` response.

        With `mode=oneshot`, a finished operation's record is deleted after
        this response, so the result can be fetched exactly once and needs
        no separate `/v1/operation/delete/` call. An `IN_PROGRESS`
        operation is never deleted. The default, `mode=persistent`, never
        deletes.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `MISSING_QUERY_PARAMETER` | No `operation_id` query parameter was given. |
        | 400 | `INVALID_QUERY_PARAMETER` | `mode` is present but not `oneshot` or `persistent`. |
        | 404 | `NOT_FOUND` | No operation with that id exists (never started, deleted, or fetched with `mode=oneshot`, or the server restarted). |
      parameters:
        - name: operation_id
          in: query
          required: true
          description: >
            Id of the operation, as received in `operation_id` from the
            `202 Accepted` response that started it.
          schema:
            type: string
            format: uuid
        - name: mode
          in: query
          required: false
          description: >
            Whether fetching a finished operation deletes its record
            (`oneshot`) or keeps it (`persistent`, the default).
          schema:
            type: string
            enum: [oneshot, persistent]
            default: persistent
      responses:
        "200":
          description: The operation's current record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationWithResult'
              examples:
                InProgress:
                  summary: The operation has not finished yet
                  value:
                    id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    status: "IN_PROGRESS"
                    progress: 0.42
                Succeeded:
                  summary: 'Finished: result holds the response'
                  description: >
                    A completed `/v1/scene/print/` operation. `result` is
                    the body that endpoint documents as its `200` response.
                  value:
                    id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    status: "SUCCEEDED"
                    progress: 1.0
                    result:
                      job_id: "8f14e45f-ceea-467f-a34e-9b6d2d7a90ae"
                Failed:
                  summary: 'Finished with an error: result holds the error envelope'
                  value:
                    id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    status: "FAILED"
                    progress: 1.0
                    result:
                      error:
                        code: "OPERATION_FAILED"
                        message: "Cannot print: the printer is busy"
        "400":
          description: The operation_id parameter was missing, or mode was unrecognized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingOperationId:
                  summary: "MISSING_QUERY_PARAMETER: no operation_id given"
                  value:
                    error:
                      code: "MISSING_QUERY_PARAMETER"
                      message: "Missing query parameter: operation_id"
                BadMode:
                  summary: "INVALID_QUERY_PARAMETER: mode is not oneshot or persistent"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid value for 'mode': 'always'. Must be 'oneshot' or 'persistent'."
        "404":
          description: No operation with that id exists.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                OperationNotFound:
                  summary: "NOT_FOUND: unknown operation id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Operation with given ID not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/operation/list/:
    post:
      operationId: listOperations
      summary: List operations
      tags:
        - Operations
      description: |
        List all in-progress, succeeded, and failed operations started by
        `?async=true` requests since this server was launched. Operations
        are not persisted across server restarts. An optional `operations`
        array filters the results to specific operation ids. Only statuses
        are reported. Use `/v1/operation/get/` to retrieve an operation's
        result.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 404 | `NOT_FOUND` | An id in `operations` does not correspond to any known operation. |
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                operations:
                  type: array
                  description: >
                    Operation ids to return, as received in `operation_id`
                    from the `202 Accepted` response that started each one.
                    Omitting this field, or sending an empty array, selects
                    every known operation.
                  items:
                    type: string
                    format: uuid
            examples:
              AllOperations:
                summary: List every known operation
                description: Omitting `operations` selects all operations.
                value: {}
              SpecificOperations:
                summary: List one specific operation
                value:
                  operations: ["7c9e6679-7425-40de-944b-e07fc1f90ae7"]
      responses:
        "200":
          description: >
            The requested operations, or every known operation if
            `operations` was omitted, empty, or malformed.
          content:
            application/json:
              schema:
                type: object
                required: [count, operations]
                properties:
                  count:
                    type: integer
                    description: Number of operations in the response.
                  operations:
                    type: array
                    items:
                      $ref: '#/components/schemas/Operation'
              examples:
                OneOperation:
                  value:
                    count: 1
                    operations:
                      - id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                        status: "IN_PROGRESS"
                        progress: 0.42
        "404":
          description: An id in `operations` does not correspond to any known operation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                OperationNotFound:
                  summary: "NOT_FOUND: unknown operation id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Operation with ID '7c9e6679-7425-40de-944b-e07fc1f90ae7' not found"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/operation/delete/:
    delete:
      operationId: deleteOperations
      summary: Delete operations
      tags:
        - Operations
      description: |
        Removes one or more operation records from the server. Operations
        are validated before any are deleted: if any requested operation
        does not exist, or is still `IN_PROGRESS`, nothing is deleted.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `operations` is missing, empty, or not an array of strings, or an unrecognized field is present. |
        | 404 | `NOT_FOUND` | An id in `operations` does not correspond to any known operation. Nothing was deleted. |
        | 409 | `OPERATION_IN_PROGRESS` | An operation in `operations` is still `IN_PROGRESS`. Nothing was deleted. |
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [operations]
              additionalProperties: false
              properties:
                operations:
                  type: array
                  minItems: 1
                  description: Ids of the operations to delete, as received in `operation_id` when each was started.
                  items:
                    type: string
                    format: uuid
            examples:
              Default:
                value:
                  operations: ["7c9e6679-7425-40de-944b-e07fc1f90ae7"]
      responses:
        "200":
          description: The operations were deleted. The response has no body.
        "400":
          description: The request body was not a JSON object, or did not match the request schema.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                NotAnObject:
                  summary: "JSON_PARSE_ERROR: body is not a JSON object"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Request body must be a valid JSON object"
                MissingOperations:
                  summary: "JSON_SCHEMA_MATCH_ERROR: no operations field"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Missing required property 'operations'."
                EmptyOperations:
                  summary: "JSON_SCHEMA_MATCH_ERROR: operations is an empty array"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Array should contain no fewer than 1 elements. in reference to key path: /operations"
        "404":
          description: An id in `operations` does not correspond to any known operation. Nothing was deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                OperationNotFound:
                  summary: "NOT_FOUND: unknown operation id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Operation with ID '7c9e6679-7425-40de-944b-e07fc1f90ae7' not found"
        "409":
          description: An operation in `operations` is still in progress. Nothing was deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                OperationInProgress:
                  summary: "OPERATION_IN_PROGRESS: operation has not finished"
                  value:
                    error:
                      code: "OPERATION_IN_PROGRESS"
                      message: "Cannot delete operation '7c9e6679-7425-40de-944b-e07fc1f90ae7': still in progress"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/device/list/:
    get:
      operationId: listDevices
      summary: List known devices
      tags:
        - Devices
      description: |
        Lists devices PreFormServer currently knows about: printers
        discovered on the local network, plus (if logged in via
        `/v1/login/`) Fleet Control printer groups and Dashboard-
        registered printers. Disconnected devices are not included.

        Pass `devices` in the request body to look up specific devices by
        id instead of listing every connected device. When `devices` is
        given, `can_print` is ignored, and the ids are checked in order:
        the first one that doesn't match a connected device fails the
        whole request with no devices returned.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `can_print` is present but not `true` or `false`. |
        | 404 | `NOT_FOUND` | An id in `devices` does not match any known device, or matches a device that is not currently connected. |
      parameters:
        - name: can_print
          in: query
          required: false
          description: >
            When `true`, only devices that can currently receive print
            jobs are returned (this excludes Fleet Control printer
            groups). Ignored when `devices` is given in the request body.
          schema:
            type: boolean
            default: false
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListDevicesRequest'
            examples:
              AllDevices:
                summary: Omit devices to list every connected device
                value: {}
              SpecificDevices:
                summary: Look up two specific devices by id
                value:
                  devices: ["ArtfulChameleon", "InconsequentialSnail"]
      responses:
        "200":
          description: >
            The connected devices matching the request, or every connected
            device if `devices` was omitted.
          content:
            application/json:
              schema:
                type: object
                required: [count, devices]
                properties:
                  count:
                    type: integer
                    description: Number of devices in the response.
                  devices:
                    type: array
                    items:
                      $ref: '#/components/schemas/Device'
              examples:
                Connected:
                  value:
                    count: 2
                    devices:
                      - id: "ArtfulChameleon"
                        product_name: "Form 4"
                        status: "Idle"
                        is_connected: true
                        connection_type: "WIFI"
                        ip_address: "192.168.1.42"
                        firmware_version: "1.9.0-2444"
                        is_remote_print_enabled: true
                        estimated_print_time_remaining_ms: 0
                        tank_id: "T1234567"
                        tank_material_code: "FLGPCL05"
                        cartridge_data: {}
                        ready_to_print_now: true
                      - id: "InconsequentialSnail"
                        product_name: "Printer Group"
                        status: "Idle"
                        is_connected: true
                        connection_type: "REMOTE"
                        firmware_version: ""
                        dashboard_group_id: "0f86346f-ac2c-47c1-a9e7-4888452f4fcf"
                        dashboard_queue_id: "71b46642-b2b7-4683-a534-ebd4b4ad3efe"
                        ready_to_print_now: false
                        queue_paused: false
                        has_form_auto: false
                        supported_machine_type_ids: ["DGSR-1-0", "FORM-4-0", "FS30-1-0"]
                        printers:
                          - id: "CloudyPangolin"
                            machine_type_id: "DGSR-1-0"
        "404":
          description: An id in `devices` was not found, or was found but is not connected. No devices are returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                DeviceNotFound:
                  summary: "NOT_FOUND: unknown device id"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Device with ID 'NoSuchDevice' not found"
                DeviceNotConnected:
                  summary: "NOT_FOUND: device exists but is not connected"
                  value:
                    error:
                      code: "NOT_FOUND"
                      message: "Device with ID 'ArtfulChameleon' is not connected"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/device/discover/:
    post:
      operationId: discoverDevices
      summary: Discover devices on the network
      tags:
        - Devices
      description: |
        Waits up to `timeout_seconds` for printers to announce themselves
        on the local network, then returns every connected, non-virtual
        device known afterwards. This is the same device set
        `/v1/device/list/` would return at that point. Call this endpoint
        to give newly powered-on or newly networked printers a chance to
        appear before listing them.

        Pass `printer` to target a specific device by its local network
        IP address, serial name (e.g. `ArtfulChameleon`), or a Fleet
        Control Queue id. These are the same address forms accepted by
        `printer` on `/v1/scene/print/` and `/v1/device/upload-firmware/`.
        If that device is found but is virtual, it is excluded from the
        response, which then reports zero devices.

        This can take up to `timeout_seconds` to respond, so prefer
        `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `timeout_seconds` is present but not a non-negative integer, `printer` is present but not a string, or an unrecognized field is present. |
        | 400 | `OPERATION_FAILED` | `printer` was given but no matching device was found within `timeout_seconds`. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DiscoverDevicesRequest'
            examples:
              DiscoverAll:
                summary: Wait 10 seconds, then list every connected device
                value:
                  timeout_seconds: 10
              DiscoverAtAddress:
                summary: Wait for one specific device
                value:
                  printer: "192.168.1.10"
                  timeout_seconds: 10
      responses:
        "200":
          description: The devices known after waiting for discovery to finish.
          content:
            application/json:
              schema:
                type: object
                required: [count, devices]
                properties:
                  count:
                    type: integer
                    description: Number of devices in the response.
                  devices:
                    type: array
                    items:
                      $ref: '#/components/schemas/Device'
              examples:
                Discovered:
                  value:
                    count: 1
                    devices:
                      - id: "ArtfulChameleon"
                        product_name: "Form 4"
                        status: "Idle"
                        is_connected: true
                        connection_type: "WIFI"
                        ip_address: "192.168.1.42"
                        firmware_version: "1.9.0-2444"
                        is_remote_print_enabled: true
                        estimated_print_time_remaining_ms: 0
                        tank_id: "T1234567"
                        tank_material_code: "FLGPCL05"
                        cartridge_data: {}
                        ready_to_print_now: true
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, or no device was found at the given address.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MalformedBody:
                  summary: "JSON_PARSE_ERROR: body is not JSON"
                  value:
                    error:
                      code: "JSON_PARSE_ERROR"
                      message: "Error parsing request JSON"
                NegativeTimeout:
                  summary: "JSON_SCHEMA_MATCH_ERROR: timeout_seconds is negative"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Value is below minimum. in reference to key path: /timeout_seconds"
                DeviceNotFound:
                  summary: "OPERATION_FAILED: no device found at printer within the timeout"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Couldn't find printer, timeout occurred."
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
        default:
          $ref: '#/components/responses/UnexpectedError'
  /v1/device/upload-firmware/:
    post:
      operationId: uploadFirmware
      summary: Upload firmware to a device
      tags:
        - Devices
      description: |
        Uploads a firmware file to `printer` and installs it. `printer` is
        a local network IP address, a printer serial name, or a Fleet
        Control Queue id. The request will time out after 60 seconds if the
        printer is not found.

        The target must be a locally connected, non-remote printer that
        supports firmware updates. Printers connected only via Dashboard
        (Remote) are rejected.

        This can take a while, especially finding the printer on the
        network, so prefer `?async=true` and poll the returned operation.

        ### Error codes

        | HTTP | `error.code` | Cause |
        |------|--------------|-------|
        | 400 | `INVALID_QUERY_PARAMETER` | `async` is present but not `true` or `false`. |
        | 400 | `JSON_PARSE_ERROR` | The request body is not a valid JSON object. |
        | 400 | `JSON_SCHEMA_MATCH_ERROR` | `printer` or `file_path` is missing or not a string, or an unrecognized field is present. |
        | 400 | `OPERATION_FAILED` | The printer could not be found within 60 seconds, is offline, is connected only via Dashboard (Remote), does not support firmware updates, or the upload itself failed. The message carries the underlying reason. |
      parameters:
        - $ref: '#/components/parameters/Async'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadFirmwareRequest'
            examples:
              Default:
                value:
                  printer: "Form4-TestyTest"
                  file_path: "C:\\Users\\user\\Desktop\\form4-public-1.9.0-2444.formware"
      responses:
        "200":
          description: The firmware was uploaded and installed. The response has no body.
        "202":
          description: >
            The request was accepted for asynchronous processing
            (`?async=true`). Poll the operations endpoints with the
            returned id.
          headers:
            Location:
              $ref: '#/components/headers/OperationLocation'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OperationAccepted'
              examples:
                Accepted:
                  value:
                    operation_id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    operation_url: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
                    message: >-
                      Operation accepted. Poll GET
                      /v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7
                      until 'status' is SUCCEEDED or FAILED; the endpoint's response
                      will appear in the 'result' field there.
        "400":
          description: The request was malformed, or the firmware could not be uploaded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                MissingFilePath:
                  summary: "JSON_SCHEMA_MATCH_ERROR: file_path is missing"
                  value:
                    error:
                      code: "JSON_SCHEMA_MATCH_ERROR"
                      message: "Required property 'file_path' not found. in reference to key path: /"
                PrinterNotFound:
                  summary: "OPERATION_FAILED: the printer could not be found"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Couldn't find printer, timeout occurred."
                MachineOffline:
                  summary: "OPERATION_FAILED: the printer is offline"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Cannot upload firmware: Machine is offline."
                RemoteNotSupported:
                  summary: "OPERATION_FAILED: the printer is only connected via Dashboard (Remote)"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Cannot upload firmware: Machine is connected via Dashboard (Remote)"
                UpdatesNotSupported:
                  summary: "OPERATION_FAILED: the printer does not support firmware updates"
                  value:
                    error:
                      code: "OPERATION_FAILED"
                      message: "Cannot upload firmware: Machine does not support firmware updates"
                BadAsyncValue:
                  summary: "INVALID_QUERY_PARAMETER: async is not a boolean"
                  value:
                    error:
                      code: "INVALID_QUERY_PARAMETER"
                      message: "Invalid boolean query parameter async was maybe"
        default:
          $ref: '#/components/responses/UnexpectedError'
components:
  parameters:
    Async:
      name: async
      in: query
      required: false
      description: >
        When `true`, returns `202 Accepted` immediately with the operation's
        id and its `operation_url` to poll for the result, instead of
        blocking until the operation completes.
      schema:
        type: boolean
        default: false
  headers:
    OperationLocation:
      description: >
        Server-relative URL of the accepted operation's record, identical to
        the response body's `operation_url`. `GET` it to follow progress and
        retrieve the result.
      schema:
        type: string
      example: "/v1/operation/get/?operation_id=7c9e6679-7425-40de-944b-e07fc1f90ae7"
  responses:
    UnexpectedError:
      description: >
        An error not specific to this endpoint. The shared error envelope and
        code registry apply to every failure.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      x-tags: [Errors]
      description: |
        The envelope returned by every failed request. See the *Errors*
        section of the introduction for the contract: `code` is stable and
        machine-readable, `message` is diagnostic text that may change at any
        time.
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
              description: >
                Human-readable description of what went wrong. Not part of the
                stable contract. Never match on this string.
    ErrorCode:
      type: string
      x-tags: [Errors]
      description: |
        Registry of stable error codes. New codes may be added in later
        releases (this is not a breaking change), so treat unrecognized codes
        as a generic failure.
      x-extensible-enum:
        - JSON_PARSE_ERROR
        - JSON_SCHEMA_MATCH_ERROR
        - MISSING_PARAMETER
        - INPUT_ERROR
        - MISSING_QUERY_PARAMETER
        - INVALID_QUERY_PARAMETER
        - OPERATION_FAILED
        - OPERATION_IN_PROGRESS
        - NOT_FOUND
        - ESTIMATION_NOT_SUPPORTED
        - BUILD_VOLUME_LIMIT_EXCEEDED
        - INTERNAL_ERROR
        - UNKNOWN_FAILURE
      x-enumDescriptions:
        JSON_PARSE_ERROR: The request body could not be parsed as JSON, or is not a JSON object where one is required.
        JSON_SCHEMA_MATCH_ERROR: The request body parsed as JSON but does not match the endpoint's request schema. The message includes a JSON pointer to the failing field.
        MISSING_PARAMETER: A required body field is absent. The message names the field.
        INPUT_ERROR: A field's value is unusable, such as a malformed id or a reference to a scene or resource that does not exist.
        MISSING_QUERY_PARAMETER: A required query parameter is absent. The message names the parameter.
        INVALID_QUERY_PARAMETER: A query parameter has an unparseable or out-of-range value. The message names the parameter and echoes the received value.
        OPERATION_FAILED: The requested operation started but did not complete successfully. The message carries the underlying reason.
        OPERATION_IN_PROGRESS: The resource is busy with another operation. Retry after it completes. Returned with HTTP 409.
        NOT_FOUND: The requested resource does not exist. Returned with HTTP 404.
        ESTIMATION_NOT_SUPPORTED: The scene's machine type or configuration does not support fast print time estimation. Returned with HTTP 422.
        BUILD_VOLUME_LIMIT_EXCEEDED: The request needs more scenes than the server can hold at once, so it created none. Opening a `.form` file with more build volumes than the limit returns it. Returned with HTTP 409.
        INTERNAL_ERROR: A server-side invariant was violated, not attributable to the request. Returned with HTTP 500.
        UNKNOWN_FAILURE: An unexpected exception occurred inside PreFormServer. Returned with HTTP 500. The message may be empty.
    LoginRequest:
      description: >
        Credentials for `/v1/login/`: either a Dashboard access token, or a
        username and password. A body mixing the two shapes is rejected.
      oneOf:
        - type: object
          title: "Username and password"
          required: [username, password]
          additionalProperties: false
          properties:
            username:
              type: string
              description: Formlabs Dashboard account username or email address.
            password:
              type: string
              description: Formlabs Dashboard account password.
        - type: object
          title: "Access token"
          required: [access_token]
          additionalProperties: false
          properties:
            access_token:
              type: string
              description: A previously issued Formlabs Dashboard access token.
    LoginResponse:
      type: object
      description: The session tokens for the account that just logged in.
      required: [access_token, refresh_token]
      properties:
        access_token:
          type: string
          description: Access token for the newly created session.
        refresh_token:
          type: string
          description: >
            Refresh token for the newly created session. Empty when logging
            in with `access_token` rather than a username and password.
    UserInfo:
      type: object
      description: >
        Account information for the logged-in Formlabs Dashboard user. This
        is largely a passthrough of the Dashboard's own account record, so
        it may include fields beyond the ones listed here.
      required: [username]
      properties:
        username:
          type: string
          description: Formlabs Dashboard account username or email address.
        email:
          type: string
          description: Account email address.
        id:
          type: integer
          description: Formlabs Dashboard account id.
        first_name:
          type: string
          description: Account holder's first name.
        last_name:
          type: string
          description: Account holder's last name.
    TranslateMode:
      type: string
      description: How to interpret the `position` of a translate request.
      default: ABSOLUTE
      enum:
        - ABSOLUTE
        - RELATIVE
      x-enumDescriptions:
        ABSOLUTE: Set each model's origin to exactly the given coordinates. Translating several models to the same absolute position stacks them.
        RELATIVE: Add the given offset to each model's current position, preserving the models' arrangement relative to each other.
    ScenePosition:
      type: object
      description: >
        A position within a printer's build volume, in millimeters, in user
        coordinate space.
      required: [x, y, z]
      additionalProperties: false
      properties:
        x:
          type: number
          description: >
            X position. 0 is the center of the build volume. Positive moves
            right as you face the printer.
        y:
          type: number
          description: >
            Y position. 0 is the center of the build volume. Positive moves
            away from you as you face the printer.
        z:
          type: number
          description: >
            Vertical position. 0 is the surface of the build platform.
    TranslateRequest:
      type: object
      description: >
        Moves the selected models. On SLA scenes the `z` component of
        `position` is ignored.
      required: [scene_id, position]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to translate. Omitted or empty means every
            model in the scene.
          items:
            type: string
            format: uuid
        position:
          $ref: '#/components/schemas/ScenePosition'
        mode:
          $ref: '#/components/schemas/TranslateMode'
    ScaleRequest:
      type: object
      description: Sets a uniform scale factor on the selected models.
      required: [scene_id, scale]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to scale. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
        scale:
          type: number
          exclusiveMinimum: 0
          description: >
            The scale factor to apply. Replaces each model's current scale
            outright. It is not multiplied against the existing value.
    OrientRequest:
      type: object
      description: Sets the absolute orientation of the selected models.
      required: [scene_id, orientation]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to orient. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
        orientation:
          $ref: '#/components/schemas/Orientation'
    AutoOrientMode:
      type: string
      description: Selects a specialized auto-orient algorithm.
      enum:
        - DENTAL
      x-enumDescriptions:
        DENTAL: >-
          Orients for dental appliance printing by tilting the occlusal
          (biting) plane, rather than searching for a quality-optimized
          orientation.
    AutoOrientRequest:
      description: >
        Automatically orients the selected models. Omitting `mode` runs the
        default quality-optimized algorithm. `tilt` is only meaningful
        together with `mode: "DENTAL"`.
      oneOf:
        - type: object
          title: Default
          required: [scene_id]
          additionalProperties: false
          properties:
            scene_id:
              type: string
              format: uuid
              description: The id of the scene containing the models.
            model_ids:
              type: array
              description: >
                Ids of the models to auto-orient. Omitted or empty means
                every model in the scene.
              items:
                type: string
                format: uuid
        - type: object
          title: Dental Mode
          required: [scene_id, mode]
          additionalProperties: false
          properties:
            scene_id:
              type: string
              format: uuid
              description: The id of the scene containing the models.
            model_ids:
              type: array
              description: >
                Ids of the models to auto-orient. Omitted or empty means
                every model in the scene.
              items:
                type: string
                format: uuid
            mode:
              $ref: '#/components/schemas/AutoOrientMode'
            tilt:
              type: integer
              default: 0
              description: >
                Degrees to tilt the occlusal plane. Only applies when
                `mode` is `"DENTAL"`.
    RaftType:
      type: string
      description: Which raft style to build under a model's supports.
      enum:
        - FULL_RAFT
        - MINI_RAFT
        - MINI_RAFTS_ON_BP
      x-enumDescriptions:
        FULL_RAFT: A single solid raft under the whole support structure.
        MINI_RAFT: A small raft under each touchpoint cluster instead of one solid raft.
        MINI_RAFTS_ON_BP: Mini rafts placed directly on the build platform rather than under the touchpoints.
    AutoSupportRequest:
      type: object
      description: >
        Generates supports for the selected models, replacing any
        supports they already have. Fields left out keep the scene's
        current support settings.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to auto-support. Omitted or empty means
            every model in the scene.
          items:
            type: string
            format: uuid
        density:
          type: number
          minimum: 0
          description: Support touchpoint density.
        slope_multiplier:
          type: number
          minimum: 0
          description: Scales how aggressively steep overhangs are supported.
        touchpoint_size_mm:
          type: number
          minimum: 0
          description: Diameter of each support touchpoint.
        raft_thickness_mm:
          type: number
          minimum: 0
          description: Thickness of the raft under the supports.
        height_above_raft_mm:
          type: number
          minimum: 0
          description: Vertical gap left between the raft and the model.
        z_compression_correction_mm:
          type: number
          minimum: 0
          description: Raft thickness compensation applied for print compression.
        early_layer_merge_mm:
          type: number
          minimum: 0
          description: Height over which nearby touchpoints are merged into wider bases.
        only_minima:
          type: boolean
          default: false
          description: >
            Generate only the touchpoints the model strictly needs to
            print (a faster, reduced-fidelity pass) instead of the full
            support structure.
        raft_type:
          $ref: '#/components/schemas/RaftType'
        raft_label_enabled:
          type: boolean
          description: >
            Engrave the model's name on its raft. The text is fixed when the
            supports are generated: renaming the model afterwards does not
            change it until the supports are regenerated. Pass `false` for a
            raft whose geometry does not depend on the model's name.
        breakaway_structure_enabled:
          type: boolean
          description: >
            Use breakaway supports. Only accepted when the scene's
            material supports this option.
        internal_supports_enabled:
          type: boolean
          description: Generate internal supports for enclosed cavities.
    AutoLayoutMode:
      type: string
      description: Which workspace's layout parameters to use for this call.
      default: DEFAULT
      enum:
        - DEFAULT
        - DENTAL
      x-enumDescriptions:
        DEFAULT: Use the scene's standard layout parameters.
        DENTAL: >-
          Temporarily use the scene's dental workspace parameters for
          this call, then restore the scene's previous parameters
          afterwards.
    LayoutBounds:
      type: object
      description: >
        A custom rectangular region of the build platform to lay out
        within, in user XY coordinates. All four fields are required
        together.
      required: [x_min_mm, x_max_mm, y_min_mm, y_max_mm]
      additionalProperties: false
      properties:
        x_min_mm:
          type: number
        x_max_mm:
          type: number
        y_min_mm:
          type: number
        y_max_mm:
          type: number
    OnLayoutFailure:
      type: string
      description: What to do when the layout cannot fit every requested model.
      default: FAIL_AND_LEAVE_SCENE_UNCHANGED
      enum:
        - FAIL_AND_LEAVE_SCENE_UNCHANGED
        - EJECT_A_MODEL_AND_RETRY
      x-enumDescriptions:
        FAIL_AND_LEAVE_SCENE_UNCHANGED: Fail the request with OPERATION_FAILED. The scene is left unchanged.
        EJECT_A_MODEL_AND_RETRY: >-
          Greedily remove seeded-random models from the layout and retry
          until the rest fit, up to max_ejections removals. Ejected
          models are removed from the scene.
    AutoLayoutResponse:
      type: object
      description: >
        The outcome of a layout for every requested model, in the request's
        `model_ids` order. Ids are bare UUIDs. Scene state is not included;
        fetch it with `/v1/scene/model/list/` or `/v1/scene/get/`.
      required: [laid_out_model_ids, ejected_model_ids]
      additionalProperties: false
      properties:
        laid_out_model_ids:
          type: array
          description: Requested models that remain in the scene.
          items:
            type: string
            format: uuid
        ejected_model_ids:
          type: array
          description: >
            Requested models removed from the scene to make the rest fit.
            Empty unless on_layout_failure is EJECT_A_MODEL_AND_RETRY.
          items:
            type: string
            format: uuid
    RegenerateSupportsRequest:
      type: object
      description: Which supported models to rebuild supports for.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models whose supports to regenerate. Omitted or
            empty means every model in the scene that has supports.
          items:
            type: string
            format: uuid
    AutoLayoutRequest:
      type: object
      description: >
        Arranges the selected models on the build platform. Omitting
        model_ids lays out every model in the scene.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to lay out. Omitted or empty means every
            model in the scene.
          items:
            type: string
            format: uuid
        mode:
          $ref: '#/components/schemas/AutoLayoutMode'
        model_spacing_mm:
          type: number
          minimum: 0
          default: 1.0
          description: Minimum gap to leave between models.
        allow_overlapping_supports:
          type: boolean
          default: false
          description: Allow one model's supports to intersect another model or its supports.
        lock_rotation:
          type: boolean
          default: false
          description: Only translate models and never rotate them, regardless of their individual lock.
        custom_bounds:
          $ref: '#/components/schemas/LayoutBounds'
        seed:
          type: integer
          minimum: 0
          maximum: 4294967295
          description: >
            Random seed controlling the layout algorithm. Given the same
            models and settings, the same seed reproduces the same
            arrangement. Omitted means a random seed is used each call.
        layout_attempts:
          type: integer
          minimum: 1
          maximum: 2500
          description: >
            Number of independent layout attempts to try, keeping the
            best. Defaults to an algorithm-chosen value, or 200 when seed
            is set without an explicit value (to keep seeded results
            deterministic).
        iterations_per_attempt:
          type: integer
          minimum: 1
          maximum: 250000
          description: >
            Optimization iterations per attempt. Defaults to an
            algorithm-chosen value, or 25000 when seed is set without an
            explicit value (to keep seeded results deterministic).
        on_layout_failure:
          $ref: '#/components/schemas/OnLayoutFailure'
        max_ejections:
          type: integer
          minimum: 1
          description: >
            Maximum number of models that may be ejected before the
            layout fails outright. Only valid with
            on_layout_failure: "EJECT_A_MODEL_AND_RETRY". Omitted means
            no cap.
      if:
        required: [max_ejections]
      then:
        required: [on_layout_failure]
        properties:
          on_layout_failure:
            const: EJECT_A_MODEL_AND_RETRY
    PackingMode:
      type: string
      description: Packing objective used to arrange models within the (SLS) build volume.
      default: PACK_HEIGHT
      enum:
        - PACK_HEIGHT
        - PACK_VOLUME
      x-enumDescriptions:
        PACK_HEIGHT: Minimize the height the packed models occupy.
        PACK_VOLUME: Minimize the volume the packed models occupy.
    AutoPackRequest:
      type: object
      description: >
        Packs the selected models within the build volume without
        duplicating them. Unselected models are left in place and act as
        fixed obstacles.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to pack. Omitted or empty means every
            model in the scene.
          items:
            type: string
            format: uuid
        model_spacing_mm:
          type: number
          minimum: 0
          description: >
            Minimum gap to leave between models. Floored at the
            material's visual collision offset if smaller.
        distance_from_wall_mm:
          type: number
          minimum: 0
          description: Margin to leave between models and the build volume walls.
        seed:
          type: integer
          minimum: 0
          maximum: 4294967295
          description: >
            Random seed controlling the packing algorithm. Omitted means
            a random seed is used each call.
        packing_mode:
          $ref: '#/components/schemas/PackingMode'
        on_pack_failure:
          $ref: '#/components/schemas/OnPackFailure'
    OnPackFailure:
      type: string
      description: What to do with models the packer could not place inside the build volume.
      default: FAIL_AND_LEAVE_SCENE_UNCHANGED
      enum:
        - FAIL_AND_LEAVE_SCENE_UNCHANGED
        - PARK_OUTSIDE_BUILD_VOLUME
      x-enumDescriptions:
        FAIL_AND_LEAVE_SCENE_UNCHANGED: Fail the request with OPERATION_FAILED. The scene is left unchanged.
        PARK_OUTSIDE_BUILD_VOLUME: >-
          Keep the pack. Models that fit take their packed poses; models
          that did not are parked beside the build volume, outside it,
          and reported in unpacked_model_ids. Nothing is removed from the scene.
    AutoPackResponse:
      type: object
      description: >
        The outcome of a pack for every selected visible model. Hidden
        models are never packed, so a hidden model in the selection appears
        in neither array. Ids are bare UUIDs; their order is unspecified.
        Scene state is not included; fetch it with `/v1/scene/model/list/`
        or `/v1/scene/get/`.
      required: [packed_model_ids, unpacked_model_ids]
      additionalProperties: false
      properties:
        packed_model_ids:
          type: array
          description: Selected visible models that ended inside the build volume.
          items:
            type: string
            format: uuid
        unpacked_model_ids:
          type: array
          description: >
            Selected visible models the packer could not place. Empty unless
            on_pack_failure is PARK_OUTSIDE_BUILD_VOLUME.
          items:
            type: string
            format: uuid
    LayoutOptions:
      type: object
      description: SLA layout parameters shared by the fill-build-platform operation.
      additionalProperties: false
      properties:
        model_spacing_mm:
          type: number
          minimum: 0
          default: 1.0
          description: Minimum gap to leave between models.
        placement_margin_mm:
          type: number
          minimum: 0
          default: 2.5
          description: Margin to leave between models and the build platform edge.
        allow_overlapping_supports:
          type: boolean
          default: false
          description: Allow one model's supports to intersect another model or its supports.
        lock_rotation:
          type: boolean
          default: false
          description: Only translate models and never rotate them, regardless of their individual lock.
        layout_attempts:
          type: integer
          minimum: 1
          maximum: 2500
          description: Number of independent layout attempts to try, keeping the best. Defaults to an algorithm-chosen value.
        iterations_per_attempt:
          type: integer
          minimum: 1
          maximum: 250000
          description: Optimization iterations per attempt. Defaults to an algorithm-chosen value.
    PackingOptions:
      type: object
      description: SLS packing parameters shared by the fill-build-chamber operation.
      additionalProperties: false
      properties:
        model_spacing_mm:
          type: number
          minimum: 0
          description: >
            Minimum gap to leave between models. Floored at the
            material's visual collision offset if smaller.
        distance_from_wall_mm:
          type: number
          minimum: 0
          description: Margin to leave between models and the build volume walls.
        seed:
          type: integer
          minimum: 0
          maximum: 4294967295
          description: >
            Random seed controlling the packing algorithm. Omitted means
            a random seed is used each call.
    FillBuildPlatformRequest:
      type: object
      description: >
        Duplicates the selected models and lays out the copies to fill up
        the build platform with as many copies as fit. Unselected models
        are left in place and act as fixed obstacles.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to duplicate. Omitted or empty means every
            model in the scene.
          items:
            type: string
            format: uuid
        layout_options:
          $ref: '#/components/schemas/LayoutOptions'
    FillBuildChamberRequest:
      type: object
      description: >
        Duplicates the selected models and packs the copies up to
        fill_to_height_mm. Unselected models are left in place and act as
        fixed obstacles.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to duplicate. Omitted or empty means every
            model in the scene.
          items:
            type: string
            format: uuid
        packing_options:
          $ref: '#/components/schemas/PackingOptions'
        fill_to_height_mm:
          type: number
          exclusiveMinimum: 0
          description: >
            Height, in mm above the build platform, to fill up to.
            Defaults to the scene's full build volume height.
    ModifiedModels:
      type: object
      description: >
        What an operation that changes existing models did: which models it
        changed. Ids are bare UUIDs; their order is unspecified. Model state
        is not included; fetch it with `/v1/scene/model/list/`. Check
        whether the models still fit with `/v1/scene/model/validate/in-bounds/`
        and whether they overlap other models with
        `/v1/scene/model/validate/interferences/`.
      required: [modified_model_ids]
      additionalProperties: false
      properties:
        modified_model_ids:
          type: array
          description: The selected models the operation was applied to.
          items:
            type: string
            format: uuid
    NewModelIds:
      type: object
      description: >
        The ids of the models a scene-mutating operation added, without
        describing the rest of the scene's state.
      required: [new_model_ids]
      properties:
        new_model_ids:
          type: array
          description: Ids of the newly created models, in no particular order.
          items:
            type: string
            format: uuid
    ImportModelResult:
      type: object
      description: >
        The models an import operation created. Model state is not
        included; fetch it with `/v1/scene/model/list/`.
      required: [new_models]
      properties:
        new_models:
          type: array
          description: >
            One entry per created model, in no particular order. Exactly one
            entry unless split_multi_model_file split the file into several
            models. Use each entry's source_part_name / source_part_index,
            never its array position, to identify which part of the file it
            came from.
          items:
            $ref: '#/components/schemas/ImportedModel'
    ImportedModel:
      type: object
      description: A model an import created, and which part of the imported file it came from.
      required: [model_id]
      properties:
        model_id:
          type: string
          format: uuid
          description: Id of the created model.
        source_part_name:
          type: string
          description: >
            The name of the part this model was created from, as read
            from the imported file, before any renaming applied by
            the scene or the request's name override. Parts with no
            name in the file get a generated name derived from the
            file name. Use source_part_index when part names cannot
            be trusted to identify parts.
        source_part_index:
          type: integer
          description: >
            Zero-based position of the source part within the imported
            file, counting every part in the file (including parts
            that failed to import). A single-model import reports 0.
            This is the stable identity for split assemblies whose
            parts share a name.
    ModelsCreatedFromSource:
      type: object
      description: >
        The models an operation created from one existing model: copies of
        it, bodies split out of it, or a processed model derived from it.
      required: [source_model_id, new_model_ids]
      properties:
        source_model_id:
          type: string
          format: uuid
          description: The id of the model the new models were created from.
        new_model_ids:
          type: array
          description: Ids of the models created from it, in no particular order.
          items:
            type: string
            format: uuid
    DuplicateModelsResult:
      type: object
      description: The copies a duplicate operation created, per source model.
      required: [results]
      properties:
        results:
          type: array
          description: >
            One entry per selected model, in request order. Each entry's
            new_model_ids are the copies created from that model. The
            source models themselves are unchanged.
          items:
            $ref: '#/components/schemas/ModelsCreatedFromSource'
    SplitModelBodiesResult:
      type: object
      description: The per-body models a split operation created, per source model.
      required: [results]
      properties:
        results:
          type: array
          description: >
            One entry per selected model, in request order. Each entry's
            new_model_ids are the per-body models split out of that model,
            in no particular order (each body's name carries its "(Body N)"
            index). The source model no longer exists in the scene.
          items:
            $ref: '#/components/schemas/ModelsCreatedFromSource'
    CageResult:
      type: object
      description: The cage model a caging operation created.
      required: [cage_model_id]
      properties:
        cage_model_id:
          type: string
          format: uuid
          description: >
            The id of the newly created cage model. The selected models are
            now children of this model.
    PlugCreated:
      type: object
      description: A plug model created while drilling drain holes.
      required: [plug_model_id, drilled_model_id]
      properties:
        plug_model_id:
          type: string
          format: uuid
          description: The id of the newly created plug model.
        drilled_model_id:
          type: string
          format: uuid
          description: >
            The id of the pre-existing model that was drilled: the model
            the plug was cut from and fits back into.
    Units:
      type: string
      description: The measurement units a model was defined in.
      x-extensible-enum:
        - MILLIMETERS
        - INCHES
      x-enumDescriptions:
        MILLIMETERS: Model coordinates are millimeters.
        INCHES: Model coordinates are inches.
    Lock:
      type: string
      description: >
        Orientation and position constraints on a model, respected by
        automatic layout and packing operations.
      default: FREE
      x-extensible-enum:
        - FREE
        - LOCKED_XY_FLIP_ENABLED
        - LOCKED_XY_ROTATION_FREE_TRANSLATION
        - LOCKED_ROTATION_FREE_TRANSLATION
        - FULLY_LOCKED
      x-enumDescriptions:
        FREE: Layout operations may move and rotate the model freely.
        LOCKED_XY_FLIP_ENABLED: Layout may move the model and spin it about the vertical (Z) axis, and may also flip it over; it may not tilt it otherwise.
        LOCKED_XY_ROTATION_FREE_TRANSLATION: Layout may move the model and spin it about the vertical (Z) axis, but not tilt it.
        LOCKED_ROTATION_FREE_TRANSLATION: Layout may move the model but not rotate it at all.
        FULLY_LOCKED: Layout leaves the model exactly where it is.
    EulerAngles:
      type: object
      description: >
        Orientation as Euler angles in degrees. Rotations are applied in the
        order z, then x, then y.
      required: [x, y, z]
      properties:
        x:
          type: number
          description: Rotation in degrees around the x axis (applied second).
        y:
          type: number
          description: Rotation in degrees around the y axis (applied last).
        z:
          type: number
          description: Rotation in degrees around the z axis (applied first).
    ModelProperties:
      type: object
      description: The full description of one model in a scene.
      required:
        - id
        - name
        - position
        - orientation
        - scale
        - units
        - bounding_box
        - original_file
        - visible
        - has_supports
        - is_group
        - is_cage
        - in_bounds
        - raw_mesh_hash
        - canonical_model_hash
        - lock
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier of the model within its scene.
        name:
          type: string
          description: Display name used within job preparation.
        position:
          $ref: '#/components/schemas/ScenePosition'
        orientation:
          $ref: '#/components/schemas/EulerAngles'
        scale:
          type: number
          description: Scale factor applied to the model.
        units:
          $ref: '#/components/schemas/Units'
        bounding_box:
          type: object
          description: >
            Axis-aligned bounding box of the model (including generated
            geometry such as supports) in scene coordinates. Group containers
            have an empty bounding box, serialized as all zeros.
          properties:
            min_corner:
              $ref: '#/components/schemas/ScenePosition'
            max_corner:
              $ref: '#/components/schemas/ScenePosition'
        original_file:
          type: string
          description: Path of the file the model was imported from.
        visible:
          type: boolean
          description: Whether the model is currently shown in the scene.
        has_supports:
          type: boolean
          description: Whether support structures have been generated for the model.
        is_group:
          type: boolean
          description: >
            Whether this entry is a Part Group container rather than a regular
            printable model. Group containers have zero volume and an empty
            bounding box.
        is_cage:
          type: boolean
          description: >
            Whether the group is a cage. Cages are a subset of groups, so
            `is_cage: true` implies `is_group: true`.
        in_bounds:
          type: boolean
          description: Whether the model lies entirely within the build volume.
        raw_mesh_hash:
          type: string
          description: Hash of the raw mesh data, before any transform.
        canonical_model_hash:
          type: string
          description: Hash of the mesh data together with its position, orientation, and scale.
        lock:
          $ref: '#/components/schemas/Lock'
    OpenFormRequest:
      type: object
      description: Identifies the .form file to load.
      required: [file]
      additionalProperties: false
      properties:
        file:
          type: string
          description: Path to the .form file to load.
    SceneLayerThicknessMm:
      type: string
      description: >
        Layer thickness in millimeters, always returned as a string: either a
        numeric string (e.g. "0.025") or the literal "ADAPTIVE" for adaptive
        layer thickness. Contrast with `/v1/print-setting/list/` and
        `/v1/print-setting/validate/`, whose `layer_thickness_mm` is a JSON
        number (or "ADAPTIVE").
    MachineTypeId:
      type: string
      description: Canonical machine type id, e.g. "FORM-4-0".
    MaterialCode:
      type: string
      description: Versioned material code, e.g. "FLGPBK05".
    PrintSettingCode:
      type: string
      description: Print setting profile code, e.g. "DEFAULT".
    SceneType:
      type: object
      description: >
        The machine/material/print-setting/layer-thickness combination used
        to create the scene.
      required: [machine_type, material_code, print_setting, layer_thickness_mm]
      properties:
        machine_type:
          $ref: '#/components/schemas/MachineTypeId'
        material_code:
          $ref: '#/components/schemas/MaterialCode'
        print_setting:
          $ref: '#/components/schemas/PrintSettingCode'
        layer_thickness_mm:
          $ref: '#/components/schemas/SceneLayerThicknessMm'
        custom_print_setting_id:
          type: string
          format: uuid
          description: >
            Id of the custom print setting the scene was created or updated
            from, if any (via `fps_file`). Absent when the scene uses a
            material-based print setting.
    SlaMaterialUsage:
      type: object
      description: Material usage for an SLA-type scene (e.g. Form 4).
      required: [volume_ml, unsupported_volume_ml]
      additionalProperties: false
      properties:
        volume_ml:
          type: number
          description: Total volume of models and supports in the scene, in milliliters.
        unsupported_volume_ml:
          type: number
          description: Total volume of models alone, not including supports, in milliliters.
    SlsMaterialUsage:
      type: object
      description: Material usage for an SLS-type scene (e.g. Fuse 1+, Fuse X1).
      required:
        - total_powder_ml
        - total_powder_kg
        - total_sintered_powder_ml
        - total_sintered_powder_kg
        - mass_packing_density
      additionalProperties: false
      properties:
        total_powder_ml:
          type: number
          description: Total volume of powder used, including unsintered support powder, in milliliters.
        total_powder_kg:
          type: number
          description: Total mass of powder used, including unsintered support powder, in kilograms.
        total_sintered_powder_ml:
          type: number
          description: Volume of powder that ends up sintered (part of the finished print), in milliliters.
        total_sintered_powder_kg:
          type: number
          description: Mass of powder that ends up sintered (part of the finished print), in kilograms.
        mass_packing_density:
          type: number
          description: Ratio of sintered powder mass to the total build volume.
    UnknownMaterialUsage:
      type: object
      description: >
        Material usage for a scene whose machine family this PreFormServer
        build does not recognize (machine type `UNKN-0-0`). Always empty.
      additionalProperties: false
    MaterialUsage:
      description: >
        Material consumption for the scene's models. The shape depends on the
        scene's printer family. An unrecognized family reports an empty
        object.
      oneOf:
        - $ref: '#/components/schemas/SlaMaterialUsage'
        - $ref: '#/components/schemas/SlsMaterialUsage'
        - $ref: '#/components/schemas/UnknownMaterialUsage'
    BuildVolume:
      type: object
      description: The build volume geometry for the scene's printer and material.
      required: [dimensions_mm, fillet_radius_mm]
      properties:
        dimensions_mm:
          type: object
          description: Build volume size in millimeters.
          required: [x, y, z]
          properties:
            x:
              type: number
            y:
              type: number
            z:
              type: number
        fillet_radius_mm:
          type: number
          description: Radius of the build volume's corner fillets, in millimeters.
    Scene:
      type: object
      description: A printer/material/print-setting combination and the models placed within its build volume.
      required:
        - id
        - models
        - scene_settings
        - material_usage
        - layer_count
        - build_volume
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier of the scene.
        models:
          type: array
          description: The models currently in the scene.
          items:
            $ref: '#/components/schemas/ModelProperties'
        scene_settings:
          $ref: '#/components/schemas/SceneType'
        material_usage:
          $ref: '#/components/schemas/MaterialUsage'
        layer_count:
          type: integer
          description: Number of layers the scene would slice to.
        build_volume:
          $ref: '#/components/schemas/BuildVolume'
    CreatedScene:
      type: object
      description: >
        What `/v1/scene/create/` accomplished: the id of the new scene and
        the print settings it was created with. Fetch the full scene with
        `/v1/scene/get/`.
      required: [scene_id, scene_settings]
      properties:
        scene_id:
          type: string
          format: uuid
          description: Unique identifier of the new scene.
        scene_settings:
          $ref: '#/components/schemas/SceneType'
    OpenedScene:
      description: >
        One scene created by `/v1/scene/open/`: a `CreatedScene` plus the
        scene's name within the `.form` file and the ids of the models
        loaded into it.
      allOf:
        - $ref: '#/components/schemas/CreatedScene'
        - $ref: '#/components/schemas/NewModelIds'
        - type: object
          required: [scene_name]
          properties:
            scene_name:
              type: string
              description: >
                Name of this scene within the .form file, distinguishing
                multiple print jobs saved in one file. The scene remembers it, and
                `/v1/scene/save/` writes it back.
    CreateSceneLayerThicknessMm:
      description: >
        A layer thickness in millimeters, accepted as a JSON number, the
        literal string "ADAPTIVE" for adaptive layer thickness, or a numeric
        value formatted as a string (e.g. "0.025"). "ADAPTIVE" is accepted
        only where the settings catalog offers an adaptive variant, on
        older printer models (see `/v1/print-setting/list/`).
      oneOf:
        - type: number
        - type: string
          enum: ["ADAPTIVE"]
        - type: string
          pattern: '^\d+(\.\d+)?$'
    CreateSceneManual:
      type: object
      description: A machine/material/print-setting/layer-thickness combination to create a scene for.
      required: [machine_type, material_code, layer_thickness_mm]
      additionalProperties: false
      properties:
        machine_type:
          $ref: '#/components/schemas/MachineTypeId'
        material_code:
          $ref: '#/components/schemas/MaterialCode'
        print_setting:
          $ref: '#/components/schemas/PrintSettingCode'
        layer_thickness_mm:
          $ref: '#/components/schemas/CreateSceneLayerThicknessMm'
    CreateSceneFpsFile:
      type: object
      description: >
        Create a scene from a saved .fps print settings file. Custom print
        settings are persisted so the scene's `scene_settings.custom_print_setting_id` can identify them later.
      required: [fps_file]
      additionalProperties: false
      properties:
        fps_file:
          type: string
          description: Full path to the .fps file.
    CreateSceneRequest:
      description: Either a manual machine/material/print-setting combination, or a saved .fps print settings file.
      oneOf:
        - $ref: '#/components/schemas/CreateSceneManual'
        - $ref: '#/components/schemas/CreateSceneFpsFile'
    UpdateSceneManual:
      type: object
      description: A new machine/material/print-setting/layer-thickness combination to apply to the scene.
      required: [scene_id, machine_type, material_code, layer_thickness_mm]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene to update.
        machine_type:
          $ref: '#/components/schemas/MachineTypeId'
        material_code:
          $ref: '#/components/schemas/MaterialCode'
        print_setting:
          $ref: '#/components/schemas/PrintSettingCode'
        layer_thickness_mm:
          $ref: '#/components/schemas/CreateSceneLayerThicknessMm'
    UpdateSceneFpsFile:
      type: object
      description: >
        Update a scene from a saved .fps print settings file. Custom print
        settings are persisted so the scene's `scene_settings.custom_print_setting_id` can identify them later.
      required: [scene_id, fps_file]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene to update.
        fps_file:
          type: string
          description: Full path to the .fps file.
    UpdateSceneRequest:
      description: >
        Either a new manual machine/material/print-setting combination, or a
        saved .fps print settings file, plus the `scene_id` to update.
      oneOf:
        - $ref: '#/components/schemas/UpdateSceneManual'
        - $ref: '#/components/schemas/UpdateSceneFpsFile'
    SaveSceneRequest:
      type: object
      description: One or more scenes to save as a .form file.
      required: [file, scene_ids]
      additionalProperties: false
      properties:
        file:
          type: string
          minLength: 1
          description: Path to write the .form file to.
        scene_ids:
          type: array
          minItems: 1
          description: >
            Ids of the scenes to save, in order. A single id saves a
            single-build-volume .form file. More than one saves a
            multi-build-volume project file.
          items:
            type: string
            format: uuid
        project_name:
          type: string
          description: >
            Name of the project inside the file, returned by
            `/v1/scene/open/`. Omitted means no name is stored.
    RenderViewType:
      type: string
      description: Camera framing for a render.
      default: ZOOM_ON_MODELS
      enum:
        - ZOOM_ON_MODELS
        - FULL_BUILD_VOLUME
        - FULL_PLATFORM_WIDTH
      x-enumDescriptions:
        ZOOM_ON_MODELS: Frame the camera to fit the selected, visible models.
        FULL_BUILD_VOLUME: Frame the camera to fit the whole build volume.
        FULL_PLATFORM_WIDTH: Frame the camera to the platform's full width, at whatever height that implies.
    FlyaroundKeyframe:
      type: object
      description: One frame of an explicit flyaround camera path.
      required: [yaw, pitch]
      additionalProperties: false
      properties:
        yaw:
          type: number
          description: Camera yaw for this frame, in degrees.
        pitch:
          type: number
          description: Camera pitch for this frame, in degrees.
    FlyaroundTransform:
      type: object
      description: >
        Renders a `.webp` animation instead of a single `.png`. Provide
        either an explicit `transform` keyframe list, or `frame_count` to
        auto-generate evenly-spaced frames around a full turn starting from
        the top-level `yaw`/`pitch`. At least one of the two is required.
      additionalProperties: false
      anyOf:
        - required: [transform]
        - required: [frame_count]
      properties:
        transform:
          type: array
          minItems: 2
          description: Explicit list of camera keyframes to render, one per animation frame.
          items:
            $ref: '#/components/schemas/FlyaroundKeyframe'
        frame_count:
          type: integer
          minimum: 1
          description: Number of evenly-spaced frames to render around a full 360-degree turn. Ignored if `transform` is present.
        ensure_constant_size:
          type: boolean
          default: true
          description: When true, every frame is rendered at the same size (the largest needed by any frame) instead of each frame being cropped independently.
    RenderRequest:
      type: object
      description: Identifies the scene to render and where to save the image.
      required: [scene_id, file]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene to render.
        file:
          type: string
          pattern: '^.*\.(png|webp)$'
          description: Path to save the rendered image to. Must end in `.png` or `.webp`. `.webp` is required when `flyaround_transform` is present.
        model_ids:
          description: >
            Ids of the models to make visible in the render (and candidates
            for `ZOOM_ON_MODELS` framing). Omitted or `[]` means every model
            in the scene.
          type: array
          items:
            type: string
            format: uuid
          default: []
        view_type:
          $ref: '#/components/schemas/RenderViewType'
        image_size_px:
          type: integer
          default: 820
          description: Maximum image width/height, in pixels. The render is square.
        crop_to_models:
          type: boolean
          default: true
          description: Whether to crop the frame tightly around the visible models instead of leaving margin.
        yaw:
          type: number
          default: 45.0
          description: Camera yaw, in degrees. Ignored when `flyaround_transform.transform` is present.
        pitch:
          type: number
          default: 35.264
          description: Camera pitch, in degrees. Ignored when `flyaround_transform.transform` is present.
        flyaround_transform:
          $ref: '#/components/schemas/FlyaroundTransform'
    ManifoldCheckStatus:
      type: string
      description: >
        The manifold library's verdict on a mesh. Only `MANIFOLD` means the
        mesh is a valid manifold solid.
      x-extensible-enum:
        - MANIFOLD
        - NOT_MANIFOLD
        - NON_FINITE_VERTEX
        - OTHER_ERROR
      x-enumDescriptions:
        MANIFOLD: The mesh is a closed manifold solid.
        NOT_MANIFOLD: The mesh has non-manifold geometry, such as open boundaries, holes, or edges/vertices shared inconsistently between triangles.
        NON_FINITE_VERTEX: The mesh contains a vertex with a NaN or infinite coordinate.
        OTHER_ERROR: The manifold library rejected the mesh for another reason.
    ManifoldCheckResult:
      type: object
      description: Manifold-check outcome for a single model.
      required: [is_manifold, status]
      properties:
        is_manifold:
          type: boolean
          description: >
            Whether the model's mesh is a manifold solid. Equivalent to
            `status == "MANIFOLD"`.
        status:
          $ref: '#/components/schemas/ManifoldCheckStatus'
    InBoundsResult:
      type: object
      description: Build-volume verdict for a single model.
      required: [in_bounds]
      properties:
        in_bounds:
          type: boolean
          description: >
            Whether the model, including its supports, fits inside the build
            volume at its current position, orientation and scale. The same
            verdict as the `in_bounds` field of `/v1/scene/model/list/`.
    ModelRepairResult:
      type: object
      description: Repair outcome for a single model.
      required: [is_manifold]
      properties:
        is_manifold:
          type: boolean
          description: >
            The manifold library's verdict on the repaired mesh, equivalent
            to a `/v1/scene/model/validate/manifoldness/` `status` of
            `MANIFOLD`. `false` means the best-effort repair left residual
            defects in the mesh (it was still replaced in the scene).
    CupDetectionResult:
      type: object
      description: Cup-detection outcome for a single model.
      required: [cup_count]
      properties:
        cup_count:
          type: integer
          minimum: 0
          description: >
            Number of cup cavities detected in the model's current
            orientation. 0 means no suction risk from cups.
    MinimaDetectionResult:
      type: object
      description: Unsupported-minima-detection outcome for a single model.
      required: [minima_count]
      properties:
        minima_count:
          type: integer
          minimum: 0
          description: >
            Number of unsupported local minima detected on the model. 0
            means every low point is currently supported.
    SupportednessDetectionResult:
      type: object
      description: Supportedness-detection outcome for a single model.
      required: [percent_unsupported]
      properties:
        percent_unsupported:
          type: number
          minimum: 0
          maximum: 100
          description: >
            Percentage of the model's surface that is undersupported in
            its current orientation and support configuration. 0 means
            fully supported.
    ThinWallDetectionRequest:
      type: object
      description: Detect thin walls in the selected models against a thickness threshold.
      required: [scene_id, threshold_mm]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to analyze. Omitted or empty means every
            model in the scene, including non-printable ones.
          items:
            type: string
            format: uuid
        threshold_mm:
          type: number
          exclusiveMinimum: 0
          description: >
            Wall thickness threshold, in millimeters. Regions thinner
            than this are flagged as thin.
    ThinWallDetectionResult:
      type: object
      description: Thin-wall-detection outcome for a single model.
      required: [status, has_thin_walls, thin_volume_mm3, thin_region_count, thin_regions]
      properties:
        status:
          type: string
          enum: [ANALYZED, TOO_SMALL, TOO_LARGE, MESH_ERROR]
          description: >
            Per-model analysis outcome. `ANALYZED`: analysis ran (see the other
            fields). `TOO_SMALL`: too small/thin to voxelize (sub-~2-voxel extent
            or sub-voxel shell). `TOO_LARGE`: too large to voxelize within the
            memory budget at this resolution (often a units mistake). `MESH_ERROR`:
            the mesh could not be rasterized (degenerate/broken geometry). For any
            non-`ANALYZED` status the model could not be assessed: `has_thin_walls`
            is false and `thin_regions` is empty. These are per-model outcomes, not
            request errors, and other models still return normally.
        has_thin_walls:
          type: boolean
          description: Whether any thin region was found.
        thin_volume_mm3:
          type: number
          description: Sum of all detected thin regions' volumes, in cubic millimeters. 0 if `has_thin_walls` is false.
        thin_region_count:
          type: integer
          minimum: 0
          description: Number of distinct thin regions detected, equal to the length of `thin_regions`.
        thin_regions:
          type: array
          description: Per-region breakdown, sorted by volume descending. Empty if `has_thin_walls` is false.
          items:
            type: object
            required: [volume_mm3, bounding_box]
            properties:
              volume_mm3:
                type: number
                description: Volume of this connected thin region, in cubic millimeters.
              bounding_box:
                type: object
                description: Axis-aligned bounding box of the region, in scene coordinates.
                properties:
                  min_corner:
                    $ref: '#/components/schemas/ScenePosition'
                  max_corner:
                    $ref: '#/components/schemas/ScenePosition'
    OperationAccepted:
      type: object
      description: >
        Returned with `202 Accepted` by endpoints called with `?async=true`.
        Poll `operation_url` (also sent as the `Location` response header)
        until the operation's `status` is terminal. Its `result` field then
        holds the response the endpoint would have returned synchronously.
      required: [operation_id, operation_url, message]
      properties:
        operation_id:
          type: string
          format: uuid
          description: Randomly generated id of the tracked operation.
        operation_url:
          type: string
          description: >
            Server-relative URL of this operation's record:
            `/v1/operation/get/?operation_id=<operation_id>`. `GET` it to
            follow progress and retrieve the result.
        message:
          type: string
          description: >
            Human-readable pointer to where the result will appear. Purely
            informational. Use `operation_url` programmatically.
    Operation:
      type: object
      description: The status of a long-running operation started by a `?async=true` request.
      required: [id, status, progress]
      properties:
        id:
          type: string
          format: uuid
          description: The operation's id, as returned in `operation_id` when it was started.
        status:
          $ref: '#/components/schemas/OperationStatus'
        progress:
          type: number
          format: float
          description: Progress of the operation, from 0.0 to 1.0.
    OperationWithResult:
      description: >
        One operation's full record, including (once the operation has
        finished) the result the originating endpoint would have returned
        synchronously.
      allOf:
        - $ref: '#/components/schemas/Operation'
        - type: object
          properties:
            result:
              type: object
              description: >
                Present once `status` is `SUCCEEDED` or `FAILED`, absent
                while `IN_PROGRESS`. On success this is the JSON body the
                originating endpoint documents as its `200` response. On
                failure it is the standard error envelope (`error.code`,
                `error.message`). Endpoints whose synchronous success
                response has no body leave `result` absent even after
                success.
    OperationStatus:
      type: string
      description: The current state of a long-running operation.
      x-extensible-enum:
        - IN_PROGRESS
        - SUCCEEDED
        - FAILED
      x-enumDescriptions:
        IN_PROGRESS: The operation has not finished yet.
        SUCCEEDED: The operation finished successfully.
        FAILED: The operation finished with an error. The error envelope is available in the `result` field of `/v1/operation/get/`. `/v1/operation/list/` reports the status only.
    LayerThicknessMm:
      description: >
        A layer thickness in millimeters, or the literal string "ADAPTIVE" for
        adaptive layer thickness. A numeric value formatted as a string (e.g.
        "0.025") is also accepted on input.
      oneOf:
        - type: number
        - type: string
          enum: ["ADAPTIVE"]
    SceneSettings:
      type: object
      description: >
        Identifies one machine/material/print-setting/layer-thickness
        combination, as returned by `/v1/print-setting/list/`. The same
        field names are used by `/v1/print-setting/validate/`.
      required: [machine_type, material_code, print_setting, layer_thickness_mm]
      properties:
        machine_type:
          $ref: '#/components/schemas/MachineTypeId'
        material_code:
          $ref: '#/components/schemas/MaterialCode'
        print_setting:
          $ref: '#/components/schemas/PrintSettingCode'
        layer_thickness_mm:
          $ref: '#/components/schemas/LayerThicknessMm'
    MaterialSetting:
      type: object
      description: One layer-thickness/print-setting variant of a material.
      required: [label, scene_settings]
      properties:
        label:
          type: string
          description: Display label, e.g. "0.025 mm (Default settings)".
        scene_settings:
          $ref: '#/components/schemas/SceneSettings'
    PrintMaterial:
      type: object
      description: A material available on a printer type, with its settings variants.
      required: [label, description, material_settings]
      properties:
        label:
          type: string
          description: Display name, e.g. "Black V5".
        description:
          type: string
          description: Short marketing description of the material. May be empty.
        material_settings:
          type: array
          description: Every layer-thickness/print-setting variant of this material.
          items:
            $ref: '#/components/schemas/MaterialSetting'
    PrinterType:
      type: object
      description: A printer type/model and the materials it supports.
      required:
        - label
        - supported_machine_type_ids
        - supported_product_names
        - build_volume_dimensions_mm
        - materials
      properties:
        label:
          type: string
          description: Display name, e.g. "Form 4".
        supported_machine_type_ids:
          type: array
          description: Canonical machine type ids this entry covers.
          items:
            type: string
        supported_product_names:
          type: array
          description: Marketing product names this entry covers.
          items:
            type: string
        build_volume_dimensions_mm:
          type: array
          description: Build volume dimensions in millimeters, as [x, y, z].
          minItems: 3
          maxItems: 3
          items:
            type: number
        materials:
          type: array
          items:
            $ref: '#/components/schemas/PrintMaterial'
    PrintSettingsListResponse:
      type: object
      required: [printer_types]
      properties:
        printer_types:
          type: array
          description: >
            Every printer type PreForm ships print settings for, or the single
            matching connected printer's type if `compatible_with` was given.
          items:
            $ref: '#/components/schemas/PrinterType'
    PrintSettingListRequest:
      type: object
      description: >
        Optionally restricts the print-setting catalog to a single connected
        printer. An empty object (or no body at all) lists the full catalog.
      additionalProperties: false
      properties:
        compatible_with:
          type: string
          description: A connected printer's serial name.
    PrintSettingValidateRequest:
      type: object
      description: >
        A (possibly partial) machine_type/material_code/print_setting/
        layer_thickness_mm combination to check. At least 3 of the 4 fields
        must be given. The response reports valid values for the omitted one.
      minProperties: 3
      additionalProperties: false
      properties:
        machine_type:
          $ref: '#/components/schemas/MachineTypeId'
        material_code:
          $ref: '#/components/schemas/MaterialCode'
        print_setting:
          $ref: '#/components/schemas/PrintSettingCode'
        layer_thickness_mm:
          $ref: '#/components/schemas/LayerThicknessMm'
    PrintSettingValidateOptions:
      type: object
      description: >
        Valid values for the one field omitted from the request, given the
        other three. Exactly one of these four properties is present.
      properties:
        machine_types:
          type: array
          items:
            type: string
        material_codes:
          type: array
          items:
            type: string
        print_settings:
          type: array
          items:
            type: string
        layer_thicknesses_mm:
          type: array
          items:
            $ref: '#/components/schemas/LayerThicknessMm'
    PrintSettingValidateResponse:
      type: object
      required: [valid]
      properties:
        valid:
          type: boolean
          description: >
            Whether at least one shipped print setting matches the fields
            given.
        options:
          $ref: '#/components/schemas/PrintSettingValidateOptions'
    RepairBehavior:
      type: string
      description: What to do when an imported file needs mesh repair.
      default: ERROR
      enum:
        - REPAIR
        - ERROR
        - IGNORE
      x-enumDescriptions:
        REPAIR: Attempt to repair the model if needed before adding it to the scene.
        ERROR: Fail the import with OPERATION_FAILED instead of importing a model that needs repair.
        IGNORE: Import the model as-is, without repairing it.
    ImportUnits:
      type: string
      description: >
        The units to interpret the file's coordinates in.
      default: DETECTED
      enum:
        - DETECTED
        - MILLIMETERS
        - INCHES
      x-enumDescriptions:
        DETECTED: Use the file's own unit metadata when the format carries it (e.g. STEP, 3MF). Otherwise treat the file as millimeters.
        MILLIMETERS: Treat the file's coordinates as millimeters, regardless of any unit metadata in the file.
        INCHES: Treat the file's coordinates as inches, regardless of any unit metadata in the file.
    TransformMatrix:
      type: object
      description: Orientation as the linear (rotation/scale) part of a 3x3 transform matrix.
      required: [linear]
      properties:
        linear:
          type: array
          description: A 3x3 matrix, given as 3 rows of 3 numbers.
          minItems: 3
          maxItems: 3
          items:
            type: array
            minItems: 3
            maxItems: 3
            items:
              type: number
    DirectionVectors:
      type: object
      description: >
        Orientation as two vectors, in model space, that say which way is
        "up" and "right" once the model is placed in the scene.
      required: [z_direction, x_direction]
      properties:
        z_direction:
          type: array
          description: Unit vector in model space that will point up (+Z) in the scene.
          minItems: 3
          maxItems: 3
          items:
            type: number
        x_direction:
          type: array
          description: Unit vector in model space, perpendicular to z_direction, that will point right (+X) in the scene.
          minItems: 3
          maxItems: 3
          items:
            type: number
    Orientation:
      description: >
        A model orientation, given as exactly one of: Euler angles, a
        transform matrix, or a pair of direction vectors.
      oneOf:
        - $ref: '#/components/schemas/EulerAngles'
        - $ref: '#/components/schemas/TransformMatrix'
        - $ref: '#/components/schemas/DirectionVectors'
    ImportModelRequest:
      type: object
      description: Loads a file and adds it to the scene as a new model.
      required: [scene_id, file]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene to import into.
        file:
          type: string
          description: Full path to the file to load.
        repair_behavior:
          $ref: '#/components/schemas/RepairBehavior'
        name:
          type: string
          description: >
            Display name for the new model. Defaults to a name derived from
            the file name.
        units:
          $ref: '#/components/schemas/ImportUnits'
        lock:
          $ref: '#/components/schemas/Lock'
        chordal_tolerance_um:
          type: number
          default: 10
          description: >
            Tessellation chordal tolerance, in micrometers. Only affects CAD
            file imports.
        angle_tolerance_deg:
          type: number
          default: 5
          description: >
            Tessellation angle tolerance, in degrees. Only affects CAD file
            imports.
        split_multi_model_file:
          type: boolean
          default: false
          description: >
            Whether a CAD or 3MF file holding several parts is imported as
            one model per part. Off by default: the parts arrive as a single
            combined model, and `name` and `lock` apply to it. No effect on
            STL or OBJ.
    ListModelsRequest:
      type: object
      description: Identifies which models in a scene to list.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to list. Omitted or empty means every model in
            the scene.
          items:
            type: string
            format: uuid
    UpdateModelRequest:
      type: object
      description: >
        Metadata to apply to the selected models. A model's pose is changed
        through the dedicated scale/orient/translate endpoints, so pose
        fields are not accepted here.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to update. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
        name:
          type: string
          description: >
            Display name to apply. Applying to more than one model at once
            gives all of them this same name.
        units:
          $ref: '#/components/schemas/Units'
        lock:
          $ref: '#/components/schemas/Lock'
    DeleteModelRequest:
      type: object
      description: Identifies which models in a scene to delete.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to delete. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
    ReplaceModelRequest:
      type: object
      description: Names the model to replace and the model already in the scene that replaces it.
      required: [scene_id, model_id, replacement_model_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing both models.
        model_id:
          type: string
          format: uuid
          description: The id of the model to replace. It is removed from the scene.
        replacement_model_id:
          type: string
          format: uuid
          description: >
            The id of the model that takes over `model_id`'s pose and
            supports. It keeps this id and its own layout lock. Must differ
            from `model_id`.
    ReplaceWarning:
      type: string
      description: >
        A non-fatal problem carrying over the replaced model's supports to
        its new geometry.
      x-extensible-enum:
        - NOT_ALL_SUPPORTS_COMPUTED_SUCCESSFULLY
        - NOT_ALL_CONTACT_LOCATIONS_APPLIED
      x-enumDescriptions:
        NOT_ALL_SUPPORTS_COMPUTED_SUCCESSFULLY: Support generation for the new geometry did not finish. The new model may have no supports.
        NOT_ALL_CONTACT_LOCATIONS_APPLIED: Supports were generated, but not every contact location from the original model's supports could be carried over to the new geometry.
    UpdateModelWarning:
      type: object
      description: A side effect `/v1/scene/model/update/` did not perform on one model.
      required: [code, model_id]
      properties:
        code:
          type: string
          x-extensible-enum:
            - RAFT_LABEL_OUTDATED
          x-enumDescriptions:
            RAFT_LABEL_OUTDATED: This request renamed the model, and its raft was generated with `raft_label_enabled`, so the engraved name no longer matches the rename. Regenerate its supports to update the label.
        model_id:
          type: string
          format: uuid
          description: The model the warning is about.
    UpdateModelResult:
      type: object
      description: >
        What `/v1/scene/model/update/` did: the models it changed, as in
        `ModifiedModels`, and any side effects it did not perform.
      required: [modified_model_ids, warnings]
      additionalProperties: false
      properties:
        modified_model_ids:
          type: array
          description: The selected models the update was applied to.
          items:
            type: string
            format: uuid
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/UpdateModelWarning'
    ReplaceModelResult:
      type: object
      description: >
        The result of replacing a model with another model in the scene.
        The replacement's id is the request's `replacement_model_id`; fetch
        its new state with `/v1/scene/model/list/`.
      required: [warnings]
      properties:
        warnings:
          type: array
          items:
            $ref: '#/components/schemas/ReplaceWarning'
    DuplicateModelRequest:
      type: object
      description: Identifies which models in a scene to duplicate, and how many copies to make.
      required: [scene_id, count]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to duplicate. Omitted or empty means every
            model in the scene.
          items:
            type: string
            format: uuid
        count:
          type: integer
          minimum: 1
          maximum: 1000
          description: The number of copies to create of each selected model.
    CageRequest:
      type: object
      description: >
        Generates a support cage around the selected models. Fields left
        unspecified fall back to the scene's default cage-generation
        settings.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to cage. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
        cage_label:
          type: string
          description: >
            Display name for the new cage model. Defaults to an
            auto-generated name ("Cage 1", "Cage 2", ...).
        generate_mesh_label:
          type: boolean
          default: true
          description: Emboss cage_label as a physical label on the cage geometry.
        bar_spacing_mm:
          type: number
          minimum: 0
          description: Gap left between adjacent cage bars.
        bar_thickness_mm:
          type: number
          minimum: 1
          description: Thickness of each cage bar.
        bar_width_mm:
          type: number
          minimum: 1
          description: Width of each cage bar.
        distance_to_cage_mm:
          type: number
          minimum: 0
          description: Gap left between the models and the surrounding cage.
        enable_round_edges:
          type: boolean
          default: false
          description: Round off the edges of the cage bars.
        enable_square_bars:
          type: boolean
          default: true
          description: >
            Give the cage bars a rectangular cross-section rather than a
            round one.
    LabelApplicationMode:
      type: string
      description: How a label is applied to a model's surface.
      default: EMBOSS
      enum:
        - EMBOSS
        - ENGRAVE
      x-enumDescriptions:
        EMBOSS: Raise the label above the surface by depth_mm.
        ENGRAVE: Cut the label into the surface to a depth of depth_mm.
    LabelRequest:
      type: object
      description: >
        Applies a text label to the surface of the selected models, at a
        given position and orientation.
      required: [scene_id, orientation, position, label, font_size_mm, depth_mm]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to label. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
        orientation:
          $ref: '#/components/schemas/Orientation'
        position:
          $ref: '#/components/schemas/ScenePosition'
        label:
          type: string
          description: The text to render.
        application_mode:
          $ref: '#/components/schemas/LabelApplicationMode'
        font_size_mm:
          type: number
          minimum: 2
          maximum: 20
          description: Label character height.
        depth_mm:
          type: number
          minimum: 0.2
          maximum: 10
          description: >
            Height above the surface (EMBOSS) or depth into it (ENGRAVE).
    LabelResult:
      type: object
      description: The result of applying a label to one or more models.
      required: [warnings, infos]
      properties:
        warnings:
          type: array
          description: >
            Free-text, non-fatal problems encountered while applying the
            label (e.g. individual characters that could not be created).
          items:
            type: string
        infos:
          type: array
          description: Free-text informational notices, if any.
          items:
            type: string
    DrainHole:
      type: object
      description: One drain/vent hole to drill into a model.
      required: [orientation, position, diameter_mm, depth_mm]
      additionalProperties: false
      properties:
        orientation:
          $ref: '#/components/schemas/Orientation'
        position:
          $ref: '#/components/schemas/ScenePosition'
        diameter_mm:
          type: number
          exclusiveMinimum: 0
          description: Diameter of the drilled hole.
        depth_mm:
          description: >
            Fixed hole depth, or the literal string "AUTO" to infer the
            depth by searching into the model along the hole's axis.
          oneOf:
            - type: number
              exclusiveMinimum: 0
            - type: string
              enum: ["AUTO"]
        outset_mm:
          type: number
          minimum: 0
          default: 1
          description: >
            How far the hole extends outward past the model's surface (the
            reverse of the drilling direction), to cleanly cut the surface
            edge.
        create_plug:
          type: boolean
          default: false
          description: Also generate a removable plug model matching this hole.
        max_search_distance:
          type: number
          minimum: 0
          description: >
            Upper bound on the depth search. Only accepted together with
            depth_mm: "AUTO".
      if:
        properties:
          depth_mm:
            type: number
      then:
        not:
          required: [max_search_distance]
    CheckManifoldRequest:
      type: object
      description: Selects the models to check.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to check. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
    InBoundsDetectionRequest:
      type: object
      description: Selects the models to check.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to check. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
    RepairModelsRequest:
      type: object
      description: Selects the models to repair.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to repair. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
    DrillRequest:
      type: object
      description: Drills the given drain holes into the selected models.
      required: [scene_id, drain_holes]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to drill. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
        drain_holes:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/DrainHole'
    DrillResult:
      type: object
      description: The result of drilling drain holes into one or more models.
      required: [plugs, warnings, infos]
      properties:
        plugs:
          type: array
          description: >
            One entry per plug model created for holes with create_plug:
            true. Empty if no plugs were created.
          items:
            $ref: '#/components/schemas/PlugCreated'
        warnings:
          type: array
          description: >
            Free-text, non-fatal problems drilling individual holes,
            identified by their index into the request's drain_holes.
          items:
            type: string
        infos:
          type: array
          description: Free-text informational notices, if any.
          items:
            type: string
    HollowRequest:
      type: object
      description: Hollows the selected models to the given wall thickness.
      required: [scene_id, wall_thickness_mm]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to hollow. Omitted or empty means every
            model in the scene.
          items:
            type: string
            format: uuid
        wall_thickness_mm:
          type: number
          minimum: 0.1
          description: Thickness of the shell wall left after hollowing.
        feature_size_mm:
          type: number
          minimum: 0.05
          default: 1
          description: >
            Smallest feature size the generated inner surface will
            resolve. Smaller is higher quality and slower. Values above 1
            are silently clamped to 1.
        accuracy:
          type: number
          minimum: 0
          maximum: 1
          default: 0.5
          description: >
            How closely the inner surface follows the model's true offset
            surface: 1 is highest quality and slowest, 0 is fastest and
            coarsest.
    HollowResult:
      type: object
      description: >
        The outcome of hollowing for every selected model. Ids are bare
        UUIDs; their order is unspecified. Model state is not included;
        fetch it with `/v1/scene/model/list/`.
      required: [hollowed_model_ids, unhollowed_model_ids, warnings, infos]
      properties:
        hollowed_model_ids:
          type: array
          description: Selected models that were hollowed. Never empty.
          items:
            type: string
            format: uuid
        unhollowed_model_ids:
          type: array
          description: >
            Selected models that could not be hollowed and were left
            unchanged. Each one also has a line in `warnings`.
          items:
            type: string
            format: uuid
        warnings:
          type: array
          description: >
            Free-text, non-fatal problems hollowing individual models
            (e.g. wall_thickness_mm too large for a model's geometry),
            one line per model in `unhollowed_model_ids`, followed by a
            suggestion to check manifoldness when any model failed.
          items:
            type: string
        infos:
          type: array
          description: Free-text informational notices, if any.
          items:
            type: string
    DentalHollowingRequest:
      type: object
      description: Cuts, extrudes, and optionally hollows the selected models.
      required: [scene_id, extrude_distance_mm]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to process. Omitted or empty means every
            model in the scene.
          items:
            type: string
            format: uuid
        cutoff_height_mm:
          type: number
          description: >
            Absolute height of the cut plane. Mutually exclusive with
            cutoff_below_gumline_mm.
        cutoff_below_gumline_mm:
          type: number
          default: 0
          description: >
            Offset of the cut plane from each model's automatically
            detected gumline. Only used when cutoff_height_mm is omitted.
            0 (the default) cuts exactly at the gumline.
        extrude_distance_mm:
          type: number
          description: >
            Length of solid material extruded downward from the cut plane
            to create a printable base.
        hollow:
          type: boolean
          default: false
          description: Also hollow the resulting model.
        shell_thickness_mm:
          type: number
          minimum: 0
          description: >
            Thickness of the hollow shell wall. Defaults to 2.75 if
            omitted. Only used when hollow is true. Rejected otherwise.
        wall_thickness_mm:
          type: number
          minimum: 0.01
          description: >
            Thickness of the honeycomb infill walls inside the hollow
            cavity (not the outer shell, which is shell_thickness_mm).
            Defaults to 1 if omitted. Only used when hollow is true.
            Rejected otherwise.
        drain_hole_radius_mm:
          type: number
          minimum: 0
          description: >
            Minor-axis radius of the drain holes generated in the hollow
            shell. Defaults to 1.5 if omitted. Only used when hollow is
            true. Rejected otherwise.
        drain_hole_height_ratio:
          type: number
          exclusiveMinimum: 0
          description: >
            Ratio of a drain hole's major axis to drain_hole_radius_mm. 1
            (the default) produces a circular hole and larger values
            elongate it. Only used when hollow is true. Rejected
            otherwise.
        drain_hole_suppression_distance_mm:
          type: number
          minimum: 0
          description: >
            Minimum distance between generated drain holes; candidates
            closer than this to another hole are dropped. Defaults to 2
            if omitted. Only used when hollow is true. Rejected
            otherwise.
        drain_hole_max_count:
          type: integer
          minimum: 0
          description: >
            Maximum number of drain holes generated per contour on the
            bottom of the model. Defaults to 2 if omitted. Only used when
            hollow is true. Rejected otherwise.
        enable_honeycomb_infill:
          type: boolean
          default: true
          description: >
            Fill the hollow cavity with honeycomb infill. Only used when
            hollow is true. Rejected otherwise.
        enable_smooth_contour_extended_sides:
          type: boolean
          default: true
          description: Smooth contours that are extended down to the build platform when creating the base.
        keep_disconnected_mesh_components:
          type: boolean
          description: >
            Keep every disconnected piece of the cut mesh instead of
            dropping all but the largest. Defaults to the scene's
            configured default if omitted.
      allOf:
        - if:
            required: [cutoff_height_mm]
          then:
            not:
              required: [cutoff_below_gumline_mm]
        - if:
            required: [hollow]
            properties:
              hollow:
                const: true
          else:
            not:
              anyOf:
                - required: [shell_thickness_mm]
                - required: [wall_thickness_mm]
                - required: [drain_hole_radius_mm]
                - required: [drain_hole_height_ratio]
                - required: [drain_hole_suppression_distance_mm]
                - required: [drain_hole_max_count]
                - required: [enable_honeycomb_infill]
    DentalHollowingResult:
      type: object
      description: >
        The models dental-hollowing created, per source model, plus any
        non-fatal warnings.
      required: [results, warnings, infos]
      properties:
        results:
          type: array
          description: >
            One entry per selected model, in request order. Each entry's
            new_model_ids name the processed model created from that model
            (exactly one today, though the array form leaves room for
            processing that yields several). The source model no longer exists in the
            scene.
          items:
            $ref: '#/components/schemas/ModelsCreatedFromSource'
        warnings:
          type: array
          description: Free-text, non-fatal problems encountered while processing.
          items:
            type: string
        infos:
          type: array
          description: Free-text informational notices, if any.
          items:
            type: string
    SplitModelBodiesRequest:
      type: object
      description: Identifies which models in a scene to split into one model per disconnected body.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to split. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
    GetModelBodyCountRequest:
      type: object
      description: Identifies which models in a scene to get the body count of.
      required: [scene_id]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene containing the models.
        model_ids:
          type: array
          description: >
            Ids of the models to inspect. Omitted or empty means every model
            in the scene.
          items:
            type: string
            format: uuid
    BodyCountResult:
      type: object
      description: The number of disconnected mesh bodies making up a single model.
      required: [body_count]
      properties:
        body_count:
          type: integer
          description: >
            Number of disconnected bodies (connected mesh components) making
            up this model. 1 for an ordinary single-body model.
    PrintSceneRequest:
      type: object
      description: Slices a scene and uploads the job directly to a printer.
      required: [scene_id, printer, job_name]
      additionalProperties: false
      properties:
        scene_id:
          type: string
          format: uuid
          description: The id of the scene to print.
        printer:
          type: string
          description: Printer serial name, IP address, or Fleet Control Queue id.
        job_name:
          type: string
          description: Name of the job.
        find_printer_timeout_seconds:
          type: integer
          minimum: 0
          default: 30
          description: Number of seconds to wait to find the given printer.
        print_now:
          type: boolean
          description: >
            If true, the job must print immediately. This fails with
            `OPERATION_FAILED` if the printer's `ready_to_print_now` status
            is not true. If false, the job is uploaded to the printer's
            local queue instead of printing immediately. Omitting this
            field defers to the printer's current `ready_to_print_now`
            state: prints immediately if ready, queues otherwise.
    ListDevicesRequest:
      type: object
      description: >
        Optional filter for `/v1/device/list/`. Omitting the body, or
        sending one that isn't a JSON object, lists every connected
        device.
      properties:
        devices:
          type: array
          description: >
            Device ids to look up, as returned in each device's `id`.
            Every id must belong to a currently connected device, or the
            whole request fails. See the endpoint's Error codes table.
          items:
            type: string
    DiscoverDevicesRequest:
      type: object
      description: Discovery parameters for `/v1/device/discover/`.
      additionalProperties: false
      properties:
        timeout_seconds:
          type: integer
          minimum: 0
          default: 60
          description: Number of seconds to wait for devices to be found.
        printer:
          type: string
          description: >
            Address of a single device to wait for, instead of
            discovering broadly. Printer serial name, IP address, or
            Fleet Control Queue id.
    UploadFirmwareRequest:
      type: object
      description: Firmware upload parameters for `/v1/device/upload-firmware/`.
      required: [printer, file_path]
      additionalProperties: false
      properties:
        printer:
          type: string
          description: Printer serial name, IP address, or Fleet Control Queue id.
        file_path:
          type: string
          description: >
            Path, on the machine running PreFormServer, to the firmware
            `.formware` or `.formware2` file to upload.
    Device:
      description: >
        A device's current status. The shape depends on the device type,
        indicated by which fields are present. Compare against the titled
        variants below rather than trying to detect the type from `id` or
        `product_name` alone.
      oneOf:
        - $ref: '#/components/schemas/GenericDevice'
        - $ref: '#/components/schemas/FleetControlPrinterGroup'
        - $ref: '#/components/schemas/Form4FamilyDevice'
        - $ref: '#/components/schemas/Form3FamilyDevice'
        - $ref: '#/components/schemas/Fuse1Device'
        - $ref: '#/components/schemas/Form2Device'
    DeviceConnectionType:
      type: string
      x-tags: [Devices]
      description: How a device is currently connected, if known.
      x-extensible-enum:
        - UNKNOWN
        - VIRTUAL
        - REMOTE
        - USB
        - WIFI
        - ETHERNET
        - ""
      x-enumDescriptions:
        UNKNOWN: Connected locally, but the specific interface could not be determined.
        VIRTUAL: A virtual/simulated device, not a real physical printer.
        REMOTE: Connected via Formlabs Dashboard / Fleet Control, not directly on the local network.
        USB: Connected directly over USB.
        WIFI: Connected over the local Wi-Fi network.
        ETHERNET: Connected over a wired Ethernet network.
        "": No direct local connection (e.g. a Fleet Control printer group, which has no connection of its own).
    CartridgeData:
      type: object
      description: >
        Cartridge data keyed by cartridge id. Unlike every other v1
        response field, the keys inside each entry are camelCase, not
        snake_case: this object is a verbatim passthrough of the
        printer's internal cartridge tracking data.
      additionalProperties:
        type: object
        required: [cartridgeMaterialCode, cartridgeEstimatedVolumeDispensed_mL, cartridgeOriginalVolume_mL]
        properties:
          cartridgeMaterialCode:
            $ref: '#/components/schemas/MaterialCode'
          cartridgeEstimatedVolumeDispensed_mL:
            type: number
            description: Estimated volume of material dispensed from this cartridge so far, in mL.
          cartridgeOriginalVolume_mL:
            type: number
            description: The cartridge's original, unused volume, in mL.
    GenericDevice:
      type: object
      title: Generic Device
      description: A device with no more specific status information available.
      required: [id, product_name, status, is_connected, connection_type, firmware_version]
      additionalProperties: false
      properties:
        id:
          type: string
          description: The device's serial name or other stable identifier.
        product_name:
          type: string
        status:
          type: string
          description: Human-readable status message. Not machine-readable. Never match on its value.
        is_connected:
          type: boolean
        connection_type:
          $ref: '#/components/schemas/DeviceConnectionType'
        ip_address:
          type: string
          description: Present only when connected directly over the local network.
        firmware_version:
          type: string
        ready_to_print_now:
          type: boolean
          description: >
            If newly uploaded jobs print automatically instead of being
            queued. Present only for devices that can receive prints.
    FleetControlPrinterGroup:
      type: object
      title: Fleet Control Printer Group
      description: >
        A Fleet Control queue: a group of one or more printers managed
        together through a Formlabs Dashboard queue.
      required:
        - id
        - product_name
        - status
        - is_connected
        - connection_type
        - firmware_version
        - dashboard_group_id
        - dashboard_queue_id
        - ready_to_print_now
        - queue_paused
        - has_form_auto
        - supported_machine_type_ids
        - printers
      additionalProperties: false
      properties:
        id:
          type: string
          description: The group's stable identifier.
        product_name:
          type: string
        status:
          type: string
          description: Human-readable status message. Not machine-readable. Never match on its value.
        is_connected:
          type: boolean
        connection_type:
          $ref: '#/components/schemas/DeviceConnectionType'
        firmware_version:
          type: string
          description: Always empty, since a printer group has no single firmware version.
        dashboard_group_id:
          type: string
          format: uuid
          description: Id of the group on Formlabs Dashboard.
        dashboard_queue_id:
          type: string
          format: uuid
          description: Id of the Fleet Control queue on Formlabs Dashboard.
        ready_to_print_now:
          type: boolean
          description: If newly uploaded jobs print automatically instead of being queued.
        queue_paused:
          type: boolean
          description: If the queue is currently paused.
        has_form_auto:
          type: boolean
          description: If the group includes a Form Auto.
        supported_machine_type_ids:
          type: array
          items:
            $ref: '#/components/schemas/MachineTypeId'
        printers:
          type: array
          description: The individual printers currently in this group.
          items:
            type: object
            required: [id, machine_type_id]
            additionalProperties: false
            properties:
              id:
                type: string
                description: The printer's serial name.
              machine_type_id:
                $ref: '#/components/schemas/MachineTypeId'
    Form4FamilyDevice:
      type: object
      title: Form 4 or Form 4L Printer
      required:
        - id
        - product_name
        - status
        - is_connected
        - connection_type
        - ip_address
        - firmware_version
        - is_remote_print_enabled
        - estimated_print_time_remaining_ms
        - tank_id
        - tank_material_code
        - cartridge_data
        - ready_to_print_now
      additionalProperties: false
      properties:
        id:
          type: string
          description: The printer's serial name.
        product_name:
          type: string
        status:
          type: string
          description: Human-readable status message. Not machine-readable. Never match on its value.
        is_connected:
          type: boolean
        connection_type:
          $ref: '#/components/schemas/DeviceConnectionType'
        ip_address:
          type: string
        firmware_version:
          type: string
        is_remote_print_enabled:
          type: boolean
          description: If the printer can be started remotely via Formlabs Dashboard.
        estimated_print_time_remaining_ms:
          type: integer
          description: Milliseconds remaining in the current print, if any, and 0 otherwise.
        tank_id:
          type: string
        tank_material_code:
          $ref: '#/components/schemas/MaterialCode'
        cartridge_data:
          $ref: '#/components/schemas/CartridgeData'
        ready_to_print_now:
          type: boolean
          description: If newly uploaded jobs print automatically instead of being queued.
    Form3FamilyDevice:
      type: object
      title: Form 3 or Form 3L Printer
      required:
        - id
        - product_name
        - status
        - is_connected
        - connection_type
        - ip_address
        - firmware_version
        - is_remote_print_enabled
        - estimated_print_time_remaining_ms
        - tank_id
        - tank_material_code
        - cartridge_data
        - form_auto_status
        - ready_to_print_now
      additionalProperties: false
      properties:
        id:
          type: string
          description: The printer's serial name.
        product_name:
          type: string
        status:
          type: string
          description: Human-readable status message. Not machine-readable. Never match on its value.
        is_connected:
          type: boolean
        connection_type:
          $ref: '#/components/schemas/DeviceConnectionType'
        ip_address:
          type: string
        firmware_version:
          type: string
        is_remote_print_enabled:
          type: boolean
          description: If the printer can be started remotely via Formlabs Dashboard.
        estimated_print_time_remaining_ms:
          type: integer
          description: Milliseconds remaining in the current print, if any, and 0 otherwise.
        tank_id:
          type: string
        tank_material_code:
          $ref: '#/components/schemas/MaterialCode'
        cartridge_data:
          $ref: '#/components/schemas/CartridgeData'
        form_auto_status:
          type: string
          description: Human-readable Form Auto status message. Not machine-readable. Never match on its value.
        ready_to_print_now:
          type: boolean
          description: If newly uploaded jobs print automatically instead of being queued.
    Fuse1Device:
      type: object
      title: Fuse 1/1+ Printer
      required:
        - id
        - product_name
        - status
        - is_connected
        - connection_type
        - ip_address
        - firmware_version
        - is_remote_print_enabled
        - estimated_print_time_remaining_ms
        - bed_temperature_c
        - powder_level
        - printing_layer
        - printing_guid
        - cylinder_material_code
        - cylinder_serial
        - printer_material_code
        - powder_credit_g
        - ready_to_print_now
      additionalProperties: false
      properties:
        id:
          type: string
          description: The printer's serial name.
        product_name:
          type: string
        status:
          type: string
          description: Human-readable status message. Not machine-readable. Never match on its value.
        is_connected:
          type: boolean
        connection_type:
          $ref: '#/components/schemas/DeviceConnectionType'
        ip_address:
          type: string
        firmware_version:
          type: string
        is_remote_print_enabled:
          type: boolean
          description: If the printer can be started remotely via Formlabs Dashboard.
        estimated_print_time_remaining_ms:
          type: integer
          description: Milliseconds remaining in the current print, if any, and 0 otherwise.
        bed_temperature_c:
          type: number
        powder_level:
          type: string
          description: Human-readable powder level message. Not machine-readable. Never match on its value.
        printing_layer:
          type: integer
          description: Index of the layer currently printing, if any.
        printing_guid:
          type: string
          description: Id of the print job currently loaded, if any.
        cylinder_material_code:
          $ref: '#/components/schemas/MaterialCode'
        cylinder_serial:
          type: string
        printer_material_code:
          $ref: '#/components/schemas/MaterialCode'
        powder_credit_g:
          type: number
          description: Remaining powder credit, in grams.
        ready_to_print_now:
          type: boolean
          description: If newly uploaded jobs print automatically instead of being queued.
    Form2Device:
      type: object
      title: Form 2 Printer
      required:
        - id
        - product_name
        - status
        - is_connected
        - connection_type
        - ip_address
        - firmware_version
        - estimated_print_time_remaining_ms
        - tank_id
        - tank_material_code
        - cartridge_data
        - ready_to_print_now
      additionalProperties: false
      properties:
        id:
          type: string
          description: The printer's serial name.
        product_name:
          type: string
        status:
          type: string
          description: Human-readable status message. Not machine-readable. Never match on its value.
        is_connected:
          type: boolean
        connection_type:
          $ref: '#/components/schemas/DeviceConnectionType'
        ip_address:
          type: string
        firmware_version:
          type: string
        estimated_print_time_remaining_ms:
          type: integer
          description: Milliseconds remaining in the current print, if any, and 0 otherwise.
        tank_id:
          type: string
        tank_material_code:
          $ref: '#/components/schemas/MaterialCode'
        cartridge_data:
          $ref: '#/components/schemas/CartridgeData'
        ready_to_print_now:
          type: boolean
          description: If newly uploaded jobs print automatically instead of being queued.
