{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://rapidcam.app/schema/rcam-v2.schema.json",
  "title": "RapidCAM Project (.rcam) — version 2",
  "description": "Contract for the RapidCAM v2 file format. A .rcam file is JSON describing a design (not an editor session): unlike v1 it carries no selection/UI state, and it may embed the fonts its text entities use. All lengths are in millimetres, in a Y-up world frame, regardless of displayUnit. See docs/rcam-format-v2.md for the authoring guide.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "version",
    "name",
    "canvas",
    "displayUnit",
    "entities",
    "constraints",
    "dimensions"
  ],
  "properties": {
    "version": {
      "const": 2,
      "description": "Format version. Must be exactly 2. The loader upgrades version-1 files automatically."
    },
    "name": {
      "type": "string",
      "description": "Human-readable project name."
    },
    "canvas": { "$ref": "#/$defs/canvas" },
    "displayUnit": {
      "enum": ["mm", "in"],
      "description": "Unit the UI presents values in. Does NOT change the file's units — geometry is always mm."
    },
    "stockThickness": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "Stock thickness in mm. Default 10 if omitted."
    },
    "hasToolChanger": {
      "type": "boolean",
      "description": "Whether the machine has an automatic tool changer (emits T/M6). Default false."
    },
    "origin": { "$ref": "#/$defs/origin" },
    "postProcessor": {
      "enum": ["linuxcnc", "grbl"],
      "description": "G-code dialect. Default \"linuxcnc\"."
    },
    "endPosition": {
      "oneOf": [
        { "$ref": "#/$defs/vec2" },
        { "type": "null" }
      ],
      "description": "Optional end-of-program park position in work coordinates (mm); the program rapids here at safe Z before M30. { x: 0, y: 0 } parks at the WCS origin. null/omitted leaves the tool where the last toolpath ended."
    },
    "groups": {
      "type": "array",
      "items": { "$ref": "#/$defs/group" },
      "description": "Named entity groups. Default []."
    },
    "layers": {
      "type": "array",
      "items": { "$ref": "#/$defs/layer" },
      "description": "Layers. If omitted, a single \"layer-0\" / \"Default\" layer is created."
    },
    "activeLayerId": {
      "type": "string",
      "description": "Id of the active layer. Default \"layer-0\"."
    },
    "entities": {
      "type": "array",
      "items": { "$ref": "#/$defs/entity" },
      "description": "Geometry. May be empty. The reserved origin point \"__origin__\" is injected automatically and need not be authored."
    },
    "constraints": {
      "type": "array",
      "items": { "$ref": "#/$defs/constraint" },
      "description": "Parametric constraints. May be empty — geometry is valid without them."
    },
    "dimensions": {
      "type": "array",
      "items": { "$ref": "#/$defs/dimension" },
      "description": "Dimensions. Driving dimensions also act as constraints. May be empty."
    },
    "variables": {
      "type": "array",
      "items": { "$ref": "#/$defs/variable" },
      "description": "Named numeric variables referenced by dimension/pattern expressions."
    },
    "patterns": {
      "type": "array",
      "items": { "$ref": "#/$defs/pattern" },
      "description": "Linear/circular pattern definitions."
    },
    "operations": {
      "type": "array",
      "items": { "$ref": "#/$defs/operation" },
      "description": "CAM toolpath operations."
    },
    "tools": {
      "type": "array",
      "items": { "$ref": "#/$defs/tool" },
      "description": "Tool definitions embedded in the file. Operations reference these by toolId; a single tool can drive many operations. Default []."
    },
    "fonts": {
      "type": "array",
      "items": { "$ref": "#/$defs/embeddedFont" },
      "description": "Non-bundled fonts referenced by text entities, embedded so glyph outlines — and therefore toolpaths — reproduce on any machine. Bundled fonts (e.g. \"roboto-regular\") resolve by id and are omitted."
    }
  },

  "$defs": {
    "vec2": {
      "type": "object",
      "additionalProperties": false,
      "required": ["x", "y"],
      "properties": {
        "x": { "type": "number", "description": "mm" },
        "y": { "type": "number", "description": "mm (Y-up: larger = further from the front)" }
      }
    },

    "canvas": {
      "type": "object",
      "additionalProperties": false,
      "required": ["width", "height"],
      "properties": {
        "width": { "type": "number", "exclusiveMinimum": 0, "description": "Work-area width, mm." },
        "height": { "type": "number", "exclusiveMinimum": 0, "description": "Work-area height, mm." }
      }
    },

    "origin": {
      "type": "object",
      "additionalProperties": false,
      "required": ["x", "y", "z"],
      "description": "Named work-coordinate-system origin. Default front-left-top.",
      "properties": {
        "x": { "enum": ["left", "center", "right"] },
        "y": { "enum": ["front", "center", "back"] },
        "z": { "enum": ["top", "bed"] }
      }
    },

    "group": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "name", "entityIds"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string" },
        "entityIds": { "type": "array", "items": { "type": "string" } }
      }
    },

    "layer": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "name", "color", "visible", "locked"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string" },
        "color": { "type": "string", "description": "CSS hex colour, e.g. \"#cdd2da\"." },
        "visible": { "type": "boolean" },
        "locked": { "type": "boolean" }
      }
    },

    "entity": {
      "oneOf": [
        { "$ref": "#/$defs/lineEntity" },
        { "$ref": "#/$defs/circleEntity" },
        { "$ref": "#/$defs/rectangleEntity" },
        { "$ref": "#/$defs/polylineEntity" },
        { "$ref": "#/$defs/arcEntity" },
        { "$ref": "#/$defs/bezierEntity" },
        { "$ref": "#/$defs/pointEntity" },
        { "$ref": "#/$defs/textEntity" }
      ]
    },

    "lineEntity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id", "a", "b"],
      "description": "Point DOF keys: a, b (endpoints), mid (midpoint, pickable but derived).",
      "properties": {
        "type": { "const": "line" },
        "id": { "type": "string", "minLength": 1 },
        "a": { "$ref": "#/$defs/vec2" },
        "b": { "$ref": "#/$defs/vec2" },
        "isConstruction": { "type": "boolean" },
        "layerId": { "type": "string" }
      }
    },

    "circleEntity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id", "center", "radius"],
      "description": "Point DOF key: c (center). Scalar DOF key: r (radius).",
      "properties": {
        "type": { "const": "circle" },
        "id": { "type": "string", "minLength": 1 },
        "center": { "$ref": "#/$defs/vec2" },
        "radius": { "type": "number", "exclusiveMinimum": 0 },
        "isConstruction": { "type": "boolean" },
        "layerId": { "type": "string" }
      }
    },

    "rectangleEntity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id", "p0", "p1"],
      "description": "Axis-aligned. p0/p1 are opposite corners (stored normalised to min/max). Point DOF keys: bl, br, tr, tl (corners); mid_b, mid_r, mid_t, mid_l (edge midpoints); center.",
      "properties": {
        "type": { "const": "rectangle" },
        "id": { "type": "string", "minLength": 1 },
        "p0": { "$ref": "#/$defs/vec2" },
        "p1": { "$ref": "#/$defs/vec2" },
        "isConstruction": { "type": "boolean" },
        "layerId": { "type": "string" }
      }
    },

    "polylineEntity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id", "points", "closed"],
      "description": "Point DOF keys: v0, v1, ... vN (vertices); mid_0, mid_1, ... (segment midpoints). A segment can substitute for a line in line-type constraints via the entity ref \"<polylineId>#<segmentIndex>\".",
      "properties": {
        "type": { "const": "polyline" },
        "id": { "type": "string", "minLength": 1 },
        "points": { "type": "array", "items": { "$ref": "#/$defs/vec2" }, "minItems": 2 },
        "closed": { "type": "boolean" },
        "isConstruction": { "type": "boolean" },
        "layerId": { "type": "string" }
      }
    },

    "arcEntity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id", "center", "radius", "startAngle", "endAngle"],
      "description": "CCW from startAngle to endAngle (radians, world Y-up). Point DOF key: c (center); start/end pickable. Scalar DOF keys: r, sa, ea.",
      "properties": {
        "type": { "const": "arc" },
        "id": { "type": "string", "minLength": 1 },
        "center": { "$ref": "#/$defs/vec2" },
        "radius": { "type": "number", "exclusiveMinimum": 0 },
        "startAngle": { "type": "number", "description": "radians" },
        "endAngle": { "type": "number", "description": "radians" },
        "isConstruction": { "type": "boolean" },
        "layerId": { "type": "string" }
      }
    },

    "bezierEntity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id", "p0", "p1", "p2", "p3"],
      "description": "Cubic Bezier. p0=start, p1=start handle, p2=end handle, p3=end. Point DOF keys: p0, p1, p2, p3. Only p0/p3 participate in constraints; p1/p2 are drag-only.",
      "properties": {
        "type": { "const": "bezier" },
        "id": { "type": "string", "minLength": 1 },
        "p0": { "$ref": "#/$defs/vec2" },
        "p1": { "$ref": "#/$defs/vec2" },
        "p2": { "$ref": "#/$defs/vec2" },
        "p3": { "$ref": "#/$defs/vec2" },
        "isConstruction": { "type": "boolean" },
        "layerId": { "type": "string" }
      }
    },

    "pointEntity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id", "pos"],
      "description": "Single reference point. Point DOF key: p. The reserved WCS origin uses id \"__origin__\".",
      "properties": {
        "type": { "const": "point" },
        "id": { "type": "string", "minLength": 1 },
        "pos": { "$ref": "#/$defs/vec2" },
        "isConstruction": { "type": "boolean" },
        "layerId": { "type": "string" }
      }
    },

    "textEntity": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "id", "text", "fontId", "sizeMM", "position", "angle"],
      "description": "Editable text, expanded to glyph contours at CAM export. position is the baseline-left anchor. angle is CCW radians. Point DOF key: pos. If fontId is not a bundled font, the font's bytes must appear in the top-level \"fonts\" array.",
      "properties": {
        "type": { "const": "text" },
        "id": { "type": "string", "minLength": 1 },
        "text": { "type": "string" },
        "fontId": { "type": "string", "description": "e.g. \"roboto-regular\" (bundled), or a \"font-XXXXXXXX\" id present in \"fonts\"." },
        "sizeMM": { "type": "number", "exclusiveMinimum": 0 },
        "position": { "$ref": "#/$defs/vec2" },
        "angle": { "type": "number", "description": "radians" },
        "isConstruction": { "type": "boolean" },
        "layerId": { "type": "string" }
      }
    },

    "embeddedFont": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "name", "format", "data"],
      "description": "A font embedded in the file, referenced by textEntity.fontId.",
      "properties": {
        "id": { "type": "string", "minLength": 1, "description": "Stable content-addressed id, e.g. \"font-1a2b3c4d\"." },
        "name": { "type": "string", "description": "Human-readable font name." },
        "format": { "enum": ["ttf", "otf", "woff"], "description": "Container format of the embedded bytes." },
        "data": { "type": "string", "description": "Base64-encoded font bytes." }
      }
    },

    "pointRef": {
      "type": "object",
      "additionalProperties": false,
      "required": ["entityId", "key"],
      "description": "A reference to one named point DOF inside an entity. See each entity's DOF-key list.",
      "properties": {
        "entityId": { "type": "string", "minLength": 1 },
        "key": { "type": "string", "minLength": 1 }
      }
    },

    "constraint": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "type", "points", "entities"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "type": {
          "enum": [
            "coincident", "horizontal", "vertical", "parallel", "perpendicular",
            "equal", "concentric", "pointOnLine", "tangent", "pointOnArc",
            "pointOnCircle", "symmetric", "collinear", "midpoint", "angle",
            "fixedPoint", "fixed"
          ]
        },
        "points": {
          "type": "array",
          "items": { "$ref": "#/$defs/pointRef" },
          "description": "Point operands. Count/role depends on type (see docs)."
        },
        "entities": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Entity operands (entity ids, or polyline-segment refs \"<id>#<index>\"). Count/role depends on type."
        },
        "params": {
          "type": "array",
          "items": { "type": "number" },
          "description": "Type-specific numbers: angle→[radians]; fixedPoint→[x, y]."
        }
      }
    },

    "dimension": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "type", "points", "entities", "value", "driving", "offset"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "type": {
          "enum": ["distance", "horizontal", "vertical", "radius", "diameter", "angle", "arclength", "line-distance"]
        },
        "points": {
          "type": "array",
          "items": { "$ref": "#/$defs/pointRef" },
          "description": "Linear dims: the 2 measured points."
        },
        "entities": {
          "type": "array",
          "items": { "type": "string" },
          "description": "radius/diameter/arclength: 1 circle or arc id. angle/line-distance: 2 line ids."
        },
        "value": { "type": "number", "description": "mm, or radians for type \"angle\"." },
        "driving": { "type": "boolean", "description": "When true, also constrains the measurement to value." },
        "offset": { "type": "number", "description": "Placement of the dimension line/leader. Visual only." },
        "anchors": {
          "type": "array",
          "items": { "type": "number" },
          "minItems": 2,
          "maxItems": 2,
          "description": "[t1, t2] parametric anchors for line-distance extension lines."
        },
        "expr": { "type": "string", "description": "Optional formula driving value, e.g. \"width * 2\"." }
      }
    },

    "variable": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "name", "expr", "value"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", "description": "Valid identifier; referenced by expressions." },
        "expr": { "type": "string", "description": "Raw input string, e.g. \"100\", \"50mm\", \"3.5in\"." },
        "value": { "type": "number", "description": "Cached evaluated value in mm." }
      }
    },

    "pattern": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "kind", "sourceIds", "instanceIds", "params"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "kind": { "enum": ["linear", "circular"] },
        "sourceIds": { "type": "array", "items": { "type": "string" }, "description": "Original (master) entity ids." },
        "instanceIds": {
          "type": "array",
          "items": { "type": "array", "items": { "type": "string" } },
          "description": "One sub-array per generated step, listing that step's copy entity ids. Those entities must also exist in `entities`."
        },
        "params": {
          "oneOf": [
            { "$ref": "#/$defs/linearPatternParams" },
            { "$ref": "#/$defs/circularPatternParams" }
          ]
        },
        "sourceSnapshot": { "type": "number", "description": "FNV-32 hash of source geometry; staleness check. Optional." }
      }
    },

    "linearPatternParams": {
      "type": "object",
      "additionalProperties": false,
      "required": ["countX", "countY", "spacingX", "spacingY"],
      "properties": {
        "countX": { "type": "integer", "minimum": 1 },
        "countY": { "type": "integer", "minimum": 1 },
        "spacingX": { "type": "number", "description": "mm" },
        "spacingY": { "type": "number", "description": "mm" },
        "spacingXExpr": { "type": "string" },
        "spacingYExpr": { "type": "string" }
      }
    },

    "circularPatternParams": {
      "type": "object",
      "additionalProperties": false,
      "required": ["count", "cx", "cy", "totalAngle"],
      "properties": {
        "count": { "type": "integer", "minimum": 1 },
        "cx": { "type": "number", "description": "rotation centre X, mm" },
        "cy": { "type": "number", "description": "rotation centre Y, mm" },
        "totalAngle": { "type": "number", "description": "radians; 2π = full circle" }
      }
    },

    "leadDef": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type", "length"],
      "properties": {
        "type": { "enum": ["none", "linear", "arc"] },
        "length": { "type": "number", "description": "mm" }
      }
    },

    "tabDef": {
      "type": "object",
      "additionalProperties": false,
      "required": ["enabled", "count", "width", "height"],
      "properties": {
        "enabled": { "type": "boolean" },
        "count": { "type": "integer", "minimum": 0 },
        "width": { "type": "number", "description": "arc-length of each tab, mm" },
        "height": { "type": "number", "description": "material left standing above the floor, mm" }
      }
    },

    "regionRef": {
      "type": "object",
      "additionalProperties": false,
      "required": ["containingLoops"],
      "description": "A pocket target region, identified by the loops that enclose it (each loop = the entity ids whose geometry forms it). Resolved against live geometry at toolpath time so it survives constraint-driven reflow.",
      "properties": {
        "containingLoops": {
          "type": "array",
          "items": { "type": "array", "items": { "type": "string" } },
          "description": "Entity-id sets of the loops enclosing the region; one inner array per loop. The face lies inside exactly these loops and outside all others."
        }
      }
    },

    "operation": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id", "name", "type", "entityIds", "side", "toolType", "toolNumber",
        "diameter", "feedrate", "plungeRate", "spindleSpeed", "safeZ",
        "depth", "stepdown", "stepover"
      ],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string" },
        "type": { "enum": ["profile", "engrave", "drill", "pocket", "chamfer"] },
        "entityIds": { "type": "array", "items": { "type": "string" }, "description": "Target geometry ids." },
        "side": { "enum": ["outside", "inside"], "description": "Profile only; ignored otherwise." },
        "toolId": { "type": "string", "description": "Optional reference to a tool id in the top-level \"tools\" array. When it resolves, that tool's geometry/feeds override the inline toolType/diameter/vAngle/tipAngle/feedrate/plungeRate/spindleSpeed/safeZ fields below (which remain as a fallback). toolNumber and the cut settings (depth/stepdown/stepover) always stay per-operation." },
        "toolType": { "enum": ["end-mill", "ball-nose", "v-bit", "drill"] },
        "toolNumber": { "type": "integer", "minimum": 1, "description": "T-number (1-based)." },
        "diameter": { "type": "number", "exclusiveMinimum": 0, "description": "Tool diameter, mm." },
        "vAngle": { "type": "number", "description": "V-bit included angle, degrees." },
        "tipDiameter": { "type": "number", "description": "V-bit flat tip diameter, mm." },
        "tipAngle": { "type": "number", "description": "Drill tip angle, degrees." },
        "feedrate": { "type": "number", "exclusiveMinimum": 0, "description": "mm/min" },
        "plungeRate": { "type": "number", "exclusiveMinimum": 0, "description": "mm/min" },
        "spindleSpeed": { "type": "number", "minimum": 0, "description": "rpm" },
        "safeZ": { "type": "number", "description": "mm above work surface" },
        "depth": { "type": "number", "description": "mm below surface; NEGATIVE for cuts." },
        "stepdown": { "type": "number", "exclusiveMinimum": 0, "description": "mm per depth pass (ignored for drill)." },
        "peckDepth": { "type": "number", "exclusiveMinimum": 0, "description": "Drill only: peck increment (mm). Drills the hole in steps, fully retracting between pecks (G83-style). Omitted/0 = single full-depth plunge." },
        "finishPass": { "type": "boolean", "description": "Profile/pocket: leave a thin radial skin during roughing and remove it in a final full-depth wall pass, cleaning the ridges between depth levels. Default false." },
        "finishAllowance": { "type": "number", "minimum": 0, "description": "Radial stock (mm) left on the walls during roughing and removed by the finishing pass. Only used when finishPass is true; default 0.2, clamped below the tool radius." },
        "chamferWidth": { "type": "number", "exclusiveMinimum": 0, "description": "Chamfer only: horizontal width (mm) of the bevel face. The plunge depth is derived from this and the V-bit angle (depth = width / tan(½·vAngle)). Requires a v-bit tool." },
        "chamferSide": { "enum": ["on", "outside", "inside"], "description": "Chamfer only: which side of the contour the bevel sits on (\"on\" = centred on the edge). Default \"on\"." },
        "sharpenCorners": { "type": "boolean", "description": "Chamfer only: pull the V-bit tip up into each sharp inside (convex) corner — the bevel tapers to the surface at the corner vertex — so corners come to a crisp point instead of a rounded fillet. Concave corners are left rounded (a V-bit can't reach them). Default false." },
        "coolant": { "enum": ["off", "mist", "flood"], "description": "Coolant for this operation: \"mist\" → M7, \"flood\" → M8 (M9 off). Default \"off\". Only emitted if the machine is flagged as having coolant (a machine-wide app preference)." },
        "stepover": { "type": "number", "exclusiveMinimum": 0, "maximum": 1, "description": "fraction of tool diameter." },
        "pocketStrategy": { "enum": ["offset", "raster"], "description": "Pocket only; default \"offset\"." },
        "islandIds": { "type": "array", "items": { "type": "string" }, "description": "Pocket only (legacy): entities treated as islands." },
        "regions": {
          "type": "array",
          "items": { "$ref": "#/$defs/regionRef" },
          "description": "Pocket only: the enclosed regions to clear, identified parametrically (by the loops that enclose each) so they reflow with the model. Resolved against live geometry at toolpath time. Supersedes the older absolute-coordinate seeds."
        },
        "tabs": { "$ref": "#/$defs/tabDef" },
        "leadIn": { "$ref": "#/$defs/leadDef" },
        "leadOut": { "$ref": "#/$defs/leadDef" }
      }
    },
    "tool": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "name", "toolType", "diameter", "feedrate", "plungeRate", "spindleSpeed", "safeZ"],
      "description": "A reusable tool definition referenced by operations via toolId.",
      "properties": {
        "id": { "type": "string", "minLength": 1, "description": "Unique tool id; the target of operation.toolId." },
        "name": { "type": "string", "description": "Human-readable tool name." },
        "toolType": { "enum": ["end-mill", "ball-nose", "v-bit", "drill"] },
        "diameter": { "type": "number", "exclusiveMinimum": 0, "description": "Tool diameter, mm." },
        "vAngle": { "type": "number", "description": "V-bit included angle, degrees." },
        "tipDiameter": { "type": "number", "description": "V-bit flat tip diameter, mm." },
        "tipAngle": { "type": "number", "description": "Drill tip angle, degrees." },
        "feedrate": { "type": "number", "exclusiveMinimum": 0, "description": "mm/min" },
        "plungeRate": { "type": "number", "exclusiveMinimum": 0, "description": "mm/min" },
        "spindleSpeed": { "type": "number", "minimum": 0, "description": "rpm" },
        "safeZ": { "type": "number", "description": "mm above work surface" }
      }
    }
  }
}
