AltoDocs
Developers

API documentation

Read and write AltoDocs documents programmatically — projects, documents, calculation canvases, and an MCP surface for AI agents.

AltoDocs API Documentation

REST API for AI agents to read and write structural engineering documents programmatically.

Base URL

https://europe-west1-statikdokai.cloudfunctions.net/api

Authentication

All endpoints (except GET /health) require a Bearer token:

Authorization: Bearer <API_KEY>

Generate an API key in the AltoDocs app under Settings → API Keys.

Content Model

Documents are structured as an array of blocks, each with a type and optional attrs and content:

json
{
  "type": "doc",
  "content": [
    { "type": "heading", "attrs": { "level": 2, "nodeId": "uuid" }, "content": [{ "type": "text", "text": "Title" }] },
    { "type": "paragraph", "attrs": { "nodeId": "uuid" }, "content": [{ "type": "text", "text": "Hello" }] },
    { "type": "mathCanvas3", "attrs": { "nodeId": "uuid", "canvasData": { "..." }, "showGrid": true, "showBorder": true } }
  ]
}

Common Block Types

TypeDescription
headingSection heading (attrs: level: 1–6)
paragraphText paragraph
mathCanvas3Calculation canvas (see MathCanvas Guide)
beamAnalysisBeam analysis with supports & loads
screenshotImage with annotations (see Screenshots Guide)
bulletList / orderedListLists with listItem children

Live Updates

Changes made via the API appear instantly in any open browser — no refresh needed. Cursor position is preserved.

Quick Start

bash
# Health check
curl https://europe-west1-statikdokai.cloudfunctions.net/api/health

# List documents
curl -H "Authorization: Bearer YOUR_KEY" \
  https://europe-west1-statikdokai.cloudfunctions.net/api/documents

# Get document content
curl -H "Authorization: Bearer YOUR_KEY" \
  https://europe-west1-statikdokai.cloudfunctions.net/api/documents/DOC_ID

# Evaluate a calculation canvas and write results back
curl -X POST -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"writeBack": true}' \
  https://europe-west1-statikdokai.cloudfunctions.net/api/documents/DOC_ID/math-canvases/NODE_ID/evaluate

# Change a region's display unit and precision
curl -X PUT -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"unitMemory": {"preferredUnit": "mm", "precision": 0}}' \
  https://europe-west1-statikdokai.cloudfunctions.net/api/documents/DOC_ID/math-canvases/NODE_ID/regions/REGION_ID

Further Reading

API Endpoints Reference

Base URL: https://europe-west1-statikdokai.cloudfunctions.net/api

All endpoints require Authorization: Bearer <API_KEY> unless noted.


Health

GET /health

No auth required. Returns { "status": "ok" }.


Projects

GET /projects

List the authenticated user's projects.

Response:

json
{ "count": 2, "projects": [{ "id": "abc", "sagensNavn": "Project Name", "..." }] }

POST /projects

Create a new project.

Body: { "sagensNavn": "My Project", "description": "..." }

Response: { "success": true, "id": "abc" }

GET /projects/:projectId/documents

List documents in a project.


Documents

GET /documents

List all user's documents. Optional query: ?limit=50

POST /documents

Create a new document with optional initial content.

Body:

json
{
  "title": "My Document",
  "projectId": "PROJECT_ID",
  "content": [
    { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Title" }] },
    { "type": "paragraph", "content": [{ "type": "text", "text": "Hello" }] }
  ]
}

GET /documents/:id

Get full document content as JSON.

Response:

json
{
  "type": "doc",
  "content": [
    { "type": "paragraph", "attrs": { "nodeId": "uuid" }, "content": [{ "type": "text", "text": "..." }] }
  ]
}

GET /documents/:id/toc

Get a lightweight table of contents containing only heading nodes.

Optional query: ?maxLevel=3 to limit heading depth. Default is 3.

Response:

json
{
  "documentId": "DOC_ID",
  "maxLevel": 3,
  "count": 3,
  "headings": [
    {
      "nodeId": "uuid-1",
      "level": 1,
      "path": "0",
      "text": "Chapter 1"
    },
    {
      "nodeId": "uuid-2",
      "level": 2,
      "path": "4",
      "text": "Loads"
    }
  ]
}

This endpoint is intended for AI agents and navigation tooling that need to find chapters without downloading or inspecting the full document body.

GET /documents/:id/summary

Get lightweight document metadata for agent planning.

Response:

json
{
  "documentId": "DOC_ID",
  "title": "Beam Design",
  "topLevelNodeCount": 42,
  "tocCount": 6,
  "totals": {
    "mathVariables": 14,
    "mathCanvases": 3,
    "beamAnalyses": 1,
    "beamAnalysesWithErrors": 1,
    "frameAnalyses": 0,
    "frameAnalysesWithErrors": 0,
    "screenshots": 2,
    "screenshotAnnotations": 7
  },
  "summary": "42 top-level nodes, 3 math canvases, 14 math variables, 1 beam analysis, 1 beam error.",
  "chapters": [
    {
      "chapter": 1,
      "headingNodeId": "uuid-1",
      "title": "Loads",
      "summary": "12 top-level nodes, 1 math canvas, 1 beam analysis, 1 beam error.",
      "counts": {
        "beamAnalysesWithErrors": 1
      }
    }
  ]
}

This is designed for AI agents that need a semantic map of the document before reading any chapter content.

PUT /documents/:id

Replace entire document content.

Body:

json
{
  "content": [
    { "type": "paragraph", "content": [{ "type": "text", "text": "New content" }] }
  ]
}

Response: { "success": true, "message": "Replaced document with N block(s)" }

GET /documents/:id/nodes

List all blocks with their nodeIds and types.

Range mode is also available for chapter and section reads:

  • ?afterNodeId=NODE_ID&beforeNodeId=NEXT_NODE_ID
  • ?startNodeId=NODE_ID&endNodeId=NEXT_NODE_ID
  • ?chapter=4

When range parameters are provided, the endpoint returns the matching top-level content blocks instead of the flattened node inventory.

GET /documents/:id/search?q=keyword

Search for text within the document.

query is accepted as an alias for q.

GET /documents/:id/nodes/:nodeId

Get a specific block by its nodeId.

POST /documents/:id/nodes

Insert new block(s) into the document.

Body:

json
{
  "position": "end",
  "content": [
    { "type": "paragraph", "content": [{ "type": "text", "text": "Appended" }] }
  ]
}

Position options: "end", "start", { "after": "nodeId" }, { "before": "nodeId" }

PUT /documents/:id/nodes/:nodeId

Update a specific block.

DELETE /documents/:id/nodes/:nodeId

Delete a specific block.


Calculation Canvases

See MathCanvas Guide for detailed format.

GET /documents/:id/math-canvases

List all calculation canvases in the document.

GET /documents/:id/math-canvases/:nodeId

Get normalized canvas data (regions, config).

GET /documents/:id/math-canvases/:nodeId/summary

Get semantic summary: defined variables, dependencies, expressions.

Response:

json
{
  "nodeId": "uuid",
  "regionCount": 5,
  "definitionCount": 3,
  "expressionCount": 2,
  "definitions": [
    { "variable": "L", "regionId": "uuid", "expression": "L := 6 m" }
  ],
  "dependencies": { "M_max": ["q", "L"] },
  "externalDependencies": ["q_ext"]
}

PUT /documents/:id/math-canvases/:nodeId

Update canvas attributes (canvasData, showGrid, showBorder).

Body:

json
{
  "canvasData": {
    "regions": [ "..." ],
    "autoLayout": true
  },
  "showGrid": true,
  "showBorder": true
}

POST /documents/:id/math-canvases/:nodeId/regions

Add region(s) to an existing canvas.

Body:

json
{
  "regions": [
    {
      "id": "uuid",
      "position": { "left": 0, "top": 0 },
      "content": "\\placeholder[var]{F}\\coloneq\\placeholder[value]{10\\operatorname{\\textcolor{blue}{kN}}}",
      "type": "math"
    }
  ]
}

PUT /documents/:id/math-canvases/:nodeId/regions/:regionId

Update a specific region's content, position, type, or display preferences.

Body (all fields optional — only provided fields are updated):

json
{
  "content": "\\placeholder[var]{L}\\coloneq\\placeholder[value]{9\\operatorname{\\textcolor{blue}{m}}}",
  "position": { "left": 0, "top": 120 },
  "unitMemory": {
    "preferredUnit": "mm",
    "precision": 0,
    "numberFormat": "decimal"
  }
}
FieldTypeDescription
contentstringNew LaTeX content
positionobject{ left, top } in pixels
unitMemoryobjectDisplay preferences (see MathCanvas Guide)
unitMemory.preferredUnitstring | nullConvert result to this unit
unitMemory.precisionnumberDecimal places (default 2, use 0 for integer)
unitMemory.numberFormatstring"decimal", "engineering", or "percentage"

DELETE /documents/:id/math-canvases/:nodeId/regions/:regionId

Delete a specific region from a canvas.

POST /documents/:id/math-canvases/:nodeId/evaluate

Evaluate all math regions server-side.

Body:

json
{ "writeBack": true }
  • writeBack (boolean, default false) — when true, writes computed results back into the document (visible instantly in the browser)

Response:

json
{
  "success": true,
  "evaluated": 8,
  "errors": 0,
  "ok": true,
  "writeBack": true,
  "results": [
    {
      "regionId": "def-L",
      "expression": "L := 6 m",
      "definedVar": "_L",
      "dependencies": [],
      "formattedLatex": "6.00 \\operatorname{...}{m}",
      "ok": true
    }
  ],
  "updatedRegions": [ "..." ]
}

Note: POST .../regions and PUT .../regions/:regionId also auto-evaluate after writing. The response includes an evaluation field with per-region results.


Screenshots

See Screenshots Guide for detailed annotation format.

GET /documents/:id/screenshots

List all screenshot nodes in the document.

GET /documents/:id/screenshots/:nodeId

Get screenshot metadata (image info, caption, alignment, annotation counts).

GET /documents/:id/screenshots/:nodeId/summary

Get extended summary with computed fields (total annotations, calibration status, image status).

PUT /documents/:id/screenshots/:nodeId

Update screenshot attributes (caption, alignment, dimensions, border, calibration).

Body (all fields optional):

json
{
  "caption": "Figure 3 — Foundation plan",
  "alignment": "left",
  "showBorder": false,
  "dimensionUnit": "cm",
  "calibration": { "px": 200, "mm": 1000, "scale": 5 }
}

GET /documents/:id/screenshots/:nodeId/annotations

Get the full annotations data (lines, dimensions, rectangles, circles, textBoxes, leaders).

PUT /documents/:id/screenshots/:nodeId/annotations

Replace all annotations.

Body:

json
{
  "lines": [],
  "dimensions": [
    { "id": "d1", "startX": 100, "startY": 300, "endX": 500, "endY": 300, "color": "#0000ff", "width": 2, "text": "2000 mm", "fontSize": 14, "auto": true }
  ],
  "rectangles": [],
  "circles": [],
  "textBoxes": [],
  "leaders": []
}

DELETE /documents/:id/screenshots/:nodeId/annotations

Clear all annotations.

PUT /documents/:id/screenshots/:nodeId/calibration

Set pixel-to-real-world calibration.

Body: { "px": 200, "mm": 1000, "scale": 5 }

DELETE /documents/:id/screenshots/:nodeId/calibration

Clear calibration.


Beam Analysis

GET /documents/:id/beam-analyses

List all beam analysis nodes in the document.

Response:

json
{
  "count": 1,
  "beamAnalyses": [
    {
      "nodeId": "uuid",
      "type": "beamAnalysis",
      "beam_length": 4000,
      "beam_params": { "E": 210000, "I": 8360000 },
      "units": { "length": "mm", "force": "kN", "distributed": "kN/m", "moment": "kN.m", "E": "MPa", "I": "mm4", "deflection": "mm", "stiffness": "kN/mm" },
      "supportCount": 2,
      "loadCount": 1,
      "supports": [
        { "id": "s1", "location": 0, "type": "pin" },
        { "id": "s2", "location": 4000, "type": "roller" }
      ],
      "loads": [
        { "id": "l1", "type": "UDLV", "magnitude": -10, "start_location": 0, "end_location": 4000 }
      ],
      "hasResults": true,
      "reactions": { "..." },
      "analysis_data": { "..." }
    }
  ]
}

GET /documents/:id/beam-analyses/:nodeId

Get a specific beam analysis node's config and results.

GET /documents/:id/beam-analyses/:nodeId/summary

Get detailed summary including support/load type breakdowns and load cases.

Response includes an extra summary field with:

  • supportTypes — count per type (e.g. { "pin": 1, "roller": 1 })
  • loadTypes — count per type (e.g. { "UDLV": 1 })
  • loadCases — list of distinct load case names
  • loadCaseCount — number of distinct load cases

PUT /documents/:id/beam-analyses/:nodeId

Update beam configuration. All fields optional — merges with current config.

Body:

json
{
  "beam_length": 6000,
  "beam_params": { "E": 210000, "I": 12000000 },
  "units": { "length": "mm", "force": "kN" },
  "supports": [
    { "location": 0, "type": "fixed" },
    { "location": 6000, "type": "roller" }
  ],
  "loads": [
    { "type": "point", "location": 3000, "magnitude": -50 }
  ]
}

Note: Providing supports or loads arrays replaces the entire array. To add/remove individual items, use the sub-endpoints below.

POST /documents/:id/beam-analyses/:nodeId/supports

Add one or more supports.

Body:

json
{
  "supports": [
    { "location": 2000, "type": "pin" }
  ]
}

Or a single support: { "support": { "location": 2000, "type": "roller" } }

FieldTypeDescription
locationnumberPosition along beam (in beam length units)
typestring"pin", "roller", "fixed", "moment_roller", or "y_spring"
kynumberSpring stiffness (only for y_spring type)
idstringOptional — auto-generated if omitted

PUT /documents/:id/beam-analyses/:nodeId/supports/:supportId

Update a specific support's location, type, or stiffness.

Body (all fields optional):

json
{ "location": 2500, "type": "roller" }

DELETE /documents/:id/beam-analyses/:nodeId/supports/:supportId

Delete a specific support.

POST /documents/:id/beam-analyses/:nodeId/loads

Add one or more loads.

Body:

json
{
  "loads": [
    { "type": "point", "location": 2000, "magnitude": -25 }
  ]
}

Or a single load: { "load": { "type": "moment", "location": 1000, "magnitude": 15 } }

FieldTypeDescription
typestring"point", "moment", "UDLV", or "TrapezoidalLoadV"
locationnumberPosition (for point/moment loads)
magnitudenumberLoad value (negative = downward for vertical)
start_locationnumberStart position (for distributed loads)
end_locationnumberEnd position (for distributed loads)
magnitude_endnumberEnd magnitude (for trapezoidal loads)
loadCasestringOptional load case name
factornumberOptional load factor (default 1.0)
idstringOptional — auto-generated if omitted

PUT /documents/:id/beam-analyses/:nodeId/loads/:loadId

Update a specific load.

Body (all fields optional):

json
{ "magnitude": -30, "location": 2500 }

DELETE /documents/:id/beam-analyses/:nodeId/loads/:loadId

Delete a specific load.

POST /documents/:id/beam-analyses/:nodeId/analyze

Run beam analysis by calling the Python backend. Returns reactions, internal forces, and deflection data.

Body:

json
{ "writeBack": true }
  • writeBack (boolean, default false) — when true, stores the analysis results back into the document's Y.Doc (visible instantly in the browser)

Response:

json
{
  "success": true,
  "message": "Beam analysis completed",
  "writeBack": false,
  "reactions": {
    "support_at_0": { "type": "pin", "horizontal_reaction": 0, "vertical_reaction": 20, "moment_reaction": 0 },
    "support_at_4000": { "type": "roller", "horizontal_reaction": 0, "vertical_reaction": 20, "moment_reaction": 0 }
  },
  "analysis_data": {
    "shear_force": { "max": 20, "min": -20, "absmax": 20 },
    "bending_moment": { "max": 20, "min": 0, "absmax": 20 },
    "deflection": { "max": 0, "min": -18.99, "absmax": 18.99 }
  },
  "hasPlots": true
}

Note: The /analyze endpoint requires the beam_python Cloud Function to be deployed in europe-west3. If it's not available, the endpoint returns a 502 error.


Frame Analysis

GET /documents/:id/frame-analyses

List all frame analysis nodes in the document.

Response:

json
{
  "count": 1,
  "frameAnalyses": [
    {
      "nodeId": "uuid",
      "type": "frameAnalysis",
      "frameTitle": "Portal Frame",
      "EA": 21000,
      "EI": 8400,
      "elementCount": 3,
      "supportCount": 2,
      "pointLoadCount": 1,
      "qLoadCount": 1,
      "momentLoadCount": 1,
      "elements": [ { "id": "elem-1", "location": [[0,0],[0,4]] } ],
      "supports": [ { "id": "sup-1", "node_id": 1, "type": "fixed" } ],
      "point_loads": [ { "id": "pl-1", "node_id": 2, "Fx": 10, "Fy": 0 } ],
      "q_loads": [ { "id": "ql-1", "element_id": 2, "q": -5 } ],
      "moment_loads": [ { "id": "ml-1", "node_id": 3, "Ty": 15 } ],
      "hasResults": false,
      "status": null,
      "reactions": null
    }
  ]
}

GET /documents/:id/frame-analyses/:nodeId

Get a specific frame analysis node's config and results.

GET /documents/:id/frame-analyses/:nodeId/summary

Get detailed summary including support type breakdown and total load count.

Response includes an extra summary field with:

  • supportTypes — count per type (e.g. { "fixed": 2 })
  • totalLoadCount — sum of point loads + distributed loads + moment loads

PUT /documents/:id/frame-analyses/:nodeId

Update frame configuration. All fields optional — merges with current config.

Body:

json
{
  "EA": 30000,
  "EI": 12000,
  "frameTitle": "Updated Frame",
  "elements": [
    { "id": "elem-1", "location": [[0,0],[0,5]], "EA": 25000 }
  ],
  "supports": [
    { "node_id": 1, "type": "fixed" }
  ],
  "point_loads": [
    { "node_id": 2, "Fx": 10, "Fy": -5 }
  ],
  "q_loads": [
    { "element_id": 1, "q": -8 }
  ],
  "moment_loads": [
    { "node_id": 3, "Ty": 20 }
  ]
}

Note: Providing elements, supports, point_loads, q_loads, or moment_loads arrays replaces the entire array. To add/remove individual items, use the sub-endpoints below.

POST /documents/:id/frame-analyses/:nodeId/elements

Add one or more elements.

Body:

json
{
  "elements": [
    { "id": "elem-4", "location": [[3,4],[3,0]], "EA": 25000 }
  ]
}

Or a single element: { "element": { "location": [[3,4],[3,0]] } }

FieldTypeDescription
locationnumber[][]Two-point array [[x1,y1],[x2,y2]] defining the element
EAnumberOptional axial stiffness override
EInumberOptional bending stiffness override
idstringOptional — auto-generated if omitted

PUT /documents/:id/frame-analyses/:nodeId/elements/:elementId

Update a specific element's location or stiffness properties.

Body (all fields optional):

json
{ "location": [[3,4],[3,1]], "EA": 30000 }

DELETE /documents/:id/frame-analyses/:nodeId/elements/:elementId

Delete a specific element.

POST /documents/:id/frame-analyses/:nodeId/supports

Add one or more supports.

Body:

json
{
  "supports": [
    { "node_id": 3, "type": "hinged" }
  ]
}

Or a single support: { "support": { "node_id": 3, "type": "fixed" } }

FieldTypeDescription
node_idnumberNode index where the support is placed
typestring"fixed", "hinged", or "roll"
directionnumberOptional direction angle (for roll supports)
idstringOptional — auto-generated if omitted

PUT /documents/:id/frame-analyses/:nodeId/supports/:supportId

Update a specific support's type, node, or direction.

Body (all fields optional):

json
{ "type": "roll", "direction": 90 }

DELETE /documents/:id/frame-analyses/:nodeId/supports/:supportId

Delete a specific support.

POST /documents/:id/frame-analyses/:nodeId/point-loads

Add one or more point loads.

Body:

json
{
  "point_loads": [
    { "node_id": 2, "Fx": -5, "Fy": 20 }
  ]
}

Or a single load: { "point_load": { "node_id": 2, "Fx": 10, "Fy": 0 } }

FieldTypeDescription
node_idnumberNode index where the load is applied
FxnumberHorizontal force component
FynumberVertical force component
rotationnumberOptional rotation angle
idstringOptional — auto-generated if omitted

PUT /documents/:id/frame-analyses/:nodeId/point-loads/:loadId

Update a specific point load.

Body (all fields optional):

json
{ "Fx": -10, "Fy": 25, "rotation": 45 }

DELETE /documents/:id/frame-analyses/:nodeId/point-loads/:loadId

Delete a specific point load.

POST /documents/:id/frame-analyses/:nodeId/q-loads

Add one or more distributed (q) loads.

Body:

json
{
  "q_loads": [
    { "element_id": 1, "q": -8 }
  ]
}

Or a single load: { "q_load": { "element_id": 2, "q": -5 } }

FieldTypeDescription
element_idnumberElement index the load is applied to
qnumberDistributed load magnitude (negative = downward)
idstringOptional — auto-generated if omitted

PUT /documents/:id/frame-analyses/:nodeId/q-loads/:loadId

Update a specific distributed load.

Body (all fields optional):

json
{ "q": -12, "element_id": 3 }

DELETE /documents/:id/frame-analyses/:nodeId/q-loads/:loadId

Delete a specific distributed load.

POST /documents/:id/frame-analyses/:nodeId/moment-loads

Add one or more moment loads.

Body:

json
{
  "moment_loads": [
    { "node_id": 2, "Ty": -25 }
  ]
}

Or a single load: { "moment_load": { "node_id": 3, "Ty": 15 } }

FieldTypeDescription
node_idnumberNode index where the moment is applied
TynumberMoment magnitude
idstringOptional — auto-generated if omitted

PUT /documents/:id/frame-analyses/:nodeId/moment-loads/:loadId

Update a specific moment load.

Body (all fields optional):

json
{ "Ty": -30, "node_id": 4 }

DELETE /documents/:id/frame-analyses/:nodeId/moment-loads/:loadId

Delete a specific moment load.


API Keys

POST /keys

Generate a new API key for the authenticated user.

Body: { "name": "My Agent Key" }

Response: { "key": "altodocs_...", "name": "My Agent Key" }

MathCanvas — API Guide

A MathCanvas is a live-evaluation calculation canvas embedded in your document. Each canvas contains regions — math expressions, text labels, or display-only LaTeX — that are evaluated top-to-bottom, left-to-right.


Canvas Structure

A calculation canvas in the document looks like:

json
{
  "type": "mathCanvas3",
  "attrs": {
    "canvasData": {
      "regions": [ "..." ],
      "autoLayout": true
    },
    "showGrid": true,
    "showBorder": true
  }
}
FieldTypeDescription
canvasData.regionsarrayThe calculation regions (see below)
canvasData.autoLayoutbooleanWhen true, the app auto-arranges regions vertically on first render, then clears the flag
showGridbooleanShow background grid dots
showBorderbooleanShow canvas border

Region Types

Math Region

json
{
  "id": "unique-id",
  "position": { "left": 0, "top": 0 },
  "content": "\\placeholder[var]{L}\\coloneq\\placeholder[value]{6\\operatorname{\\textcolor{blue}{m}}}",
  "type": "math",
  "unitMemory": {
    "preferredUnit": "mm",
    "precision": 0,
    "numberFormat": "decimal"
  }
}
FieldTypeDefaultDescription
unitMemory.preferredUnitstring | nullnullConvert result to this unit (e.g. "mm", "kN", "m^2")
unitMemory.precisionnumber2Number of decimal places (0 = integer display)
unitMemory.numberFormatstring"decimal""decimal", "engineering", or "percentage"
unitMemory.engineeringPowernumber3Exponent for engineering notation (e.g. 3 = x10 cubed)

Display preferences apply to results. For pure definitions (var+value), setting preferredUnit or custom precision causes the evaluator to append a converted result — e.g., A_s := 2513 mm squared = 0.00251 m squared. Definitions without unitMemory stay clean.

Text Region

json
{
  "id": "unique-id",
  "position": { "left": 0, "top": 0 },
  "content": "<b>Input Parameters</b>",
  "type": "text",
  "boundingBox": { "width": 300, "height": 36 }
}

LaTeX Region

json
{
  "id": "unique-id",
  "position": { "left": 0, "top": 0 },
  "content": "\\sigma = \\frac{M}{W}",
  "type": "latex"
}

Placeholder System (Math Regions)

Math content uses a semantic placeholder system with exactly 4 valid types:

TypePurposeExample
varVariable name being defined\placeholder[var]{L}
valueNumeric value (with optional unit)\placeholder[value]{6\operatorname{\textcolor{blue}{m}}}
exprExpression referencing other variables\placeholder[expr]{q \cdot L^2 / 8}
resultComputed result (auto-filled by the system)\placeholder[result]{}

WARNING: Only var, value, expr, result are valid. Any other type (e.g. unit, resultValue) will be rejected and the region will render as empty red squares.

Assignment Operator

Use \coloneq (renders as :=). Do NOT use := directly.


Building Expressions

Variable Definition (constant value)

Define a variable with a fixed numeric value:

\placeholder[var]{VARNAME}\coloneq\placeholder[value]{NUMBER UNIT}

Examples:

\placeholder[var]{L}\coloneq\placeholder[value]{6\operatorname{\textcolor{blue}{m}}}
\placeholder[var]{q}\coloneq\placeholder[value]{15\frac{\operatorname{\textcolor{blue}{kN}}}{\operatorname{\textcolor{blue}{m}}}}
\placeholder[var]{f_{ck}}\coloneq\placeholder[value]{25\operatorname{\textcolor{blue}{MPa}}}

Variable Definition with Computed Expression

Define a variable whose value is computed from other variables. Leave \placeholder[result]{} empty — the system evaluates it automatically:

\placeholder[var]{VARNAME}\coloneq\placeholder[expr]{EXPRESSION}=\placeholder[result]{}

Examples:

\placeholder[var]{M_{max}}\coloneq\placeholder[expr]{\frac{q\cdot L^{2}}{8}}=\placeholder[result]{}
\placeholder[var]{A_s}\coloneq\placeholder[expr]{\frac{M_{max}}{f_{ck}\cdot d}}=\placeholder[result]{}

CRITICAL: The =\placeholder[result]{} at the end is what triggers the system to compute and display the answer. Without it, nothing is evaluated. Leave the result empty — never compute it yourself.

Bare Expression (no variable name)

Evaluate an expression without assigning it to a variable:

\placeholder[expr]{0.9\cdot\left(g_{b,1}+g_{b,2}\right)}=\placeholder[result]{}

Math Operations in LaTeX

OperationLaTeXExample
Multiplication\cdotq\cdot L
Division (fraction)\frac{a}{b}\frac{q\cdot L^{2}}{8}
Power^{n}L^{2}, b^{3}
Square root\sqrt{x}\sqrt{A}
Parentheses\left( \right)\left(a+b\right)
Subscript_{sub}M_{max}, f_{ck}
Greek letters\alpha, \sigma, etc.\sigma_{s}
Pi\pi\pi\cdot d^{2}

Units

Units are embedded inline inside value or result placeholders — they are NOT separate placeholders.

Format

\operatorname{\textcolor{blue}{UNIT}}

Examples

UnitLaTeX
kN\operatorname{\textcolor{blue}{kN}}
m\operatorname{\textcolor{blue}{m}}
mm\operatorname{\textcolor{blue}{mm}}
MPa\operatorname{\textcolor{blue}{MPa}}
kN/m squared\frac{\operatorname{\textcolor{blue}{kN}}}{\operatorname{\textcolor{blue}{m}}^2}
kN/m\frac{\operatorname{\textcolor{blue}{kN}}}{\operatorname{\textcolor{blue}{m}}}
mm to the 4th\operatorname{\textcolor{blue}{mm}}^{4}
kN times m\operatorname{\textcolor{blue}{kN}}\cdot\operatorname{\textcolor{blue}{m}}

Display Preferences (Unit Conversion & Formatting)

Each region can have a unitMemory object that controls how the evaluated result is displayed. This enables:

  • Unit conversion — show a result in a different (compatible) unit
  • Precision control — set the number of decimal places (including 0 for integers)
  • Number format — decimal, engineering notation, or percentage

Example: Convert and reformat via API

bash
# Change delta_max to display in mm with 0 decimal places
curl -X PUT "$BASE/documents/$DOC/math-canvases/$CANVAS/regions/calc-delta" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "unitMemory": { "preferredUnit": "mm", "precision": 0 } }'

# Change N_Rd to display in kN with 2 decimal places
curl -X PUT "$BASE/documents/$DOC/math-canvases/$CANVAS/regions/calc-NRd" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "unitMemory": { "preferredUnit": "kN", "precision": 2 } }'

# Re-evaluate canvas to apply the new display preferences
curl -X POST "$BASE/documents/$DOC/math-canvases/$CANVAS/evaluate" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "writeBack": true }'

Supported Units

CategoryUnits
Lengthmm, cm, m, km
ForceN, kN, MN
PressurePa, kPa, MPa, GPa
Masskg, ton
Timesecond, minute, hour
Anglesdeg, rad

Compound units work too: m^2, kN/m, kN/m^2, cm^4, etc.

When results are written

Region typeHas unitMemory?Result written?
Expression (\placeholder[expr])AnyAlways
Pure definition (\placeholder[value])NoNever (no redundant re-statement)
Pure definition (\placeholder[value])Yes (preferredUnit or non-default precision)Yes (unit conversion / reformatting)

Live sync

Changes made via the API appear instantly in any open browser — no refresh needed.


Layout & Positioning (2D Canvas)

The canvas is a 2D coordinate space. Each region has:

  • position: { left, top } — top-left corner in pixels
  • boundingBox: { width, height } — rendered size in pixels (measured by the app)

Regions are positioned absolutely. You control where every region sits.

Approach 1: Auto-Layout (simple, for new canvases)

Set autoLayout: true and all positions to (0, 0). The app stacks them vertically.

Approach 2: Read-then-Position (precise control)

  1. Create the canvas (positions can be (0, 0))
  2. Wait for the app to render (browser must load the document)
  3. Read via GET /math-canvases/:nodeId — each region now has boundingBox
  4. Calculate positions from the known dimensions
  5. Update via PUT /math-canvases/:nodeId
python
y = 0
for region in regions:
    h = region["boundingBox"]["height"]
    region["position"] = {"left": 0, "top": round(y)}
    y += h + 1  # 1px gap (or 0 for flush)

Position Affects Evaluation Order

Regions are evaluated top-to-bottom (by Y), then left-to-right (by X). A variable must be defined ABOVE (lower Y) the expression that uses it. If you move an expression above its dependencies, it will show a red ? error.


Evaluation

Server-Side Evaluation

The API includes a server-side evaluation engine that uses the same math engine as the browser. This enables:

  • AI-driven computation — define variables and get results back immediately without a browser
  • Batch evaluation — evaluate entire canvases in one API call
  • Consistent results — server and browser always produce the same answers

Auto-Evaluate on Write

When you add or update regions via POST .../regions or PUT .../regions/:regionId, the server automatically evaluates all math regions and includes results in the response:

json
{
  "success": true,
  "regions": [ "..." ],
  "evaluation": {
    "evaluated": 4,
    "errors": 0,
    "results": [
      {
        "regionId": "calc-M",
        "definedVar": "_M_max",
        "formattedLatex": "54.00 \\operatorname{...}",
        "ok": true
      }
    ]
  }
}

The auto-evaluate also writes computed results into the document, so results appear instantly in the browser.

Explicit Evaluate Endpoint

POST /documents/:id/math-canvases/:nodeId/evaluate

Body:

json
{ "writeBack": true }
FieldTypeDefaultDescription
writeBackbooleanfalseWhen true, writes computed results back into the document

Response:

json
{
  "success": true,
  "evaluated": 8,
  "errors": 0,
  "ok": true,
  "writeBack": true,
  "results": [
    {
      "regionId": "def-L",
      "expression": "L := 6 m",
      "definedVar": "_L",
      "dependencies": [],
      "formattedLatex": "6.00 \\operatorname{\\textcolor{blue}{m}}",
      "ok": true
    }
  ],
  "updatedRegions": [ "..." ]
}

Browser-Side Evaluation

When content changes reach the browser (via live sync), the browser also evaluates independently. Since both server and browser use the same math engine, results always match.


Evaluation Order (detailed)

  1. Document position (earlier blocks in the document come first)
  2. Within a canvas: Y position (top-to-bottom)
  3. Same Y: X position (left-to-right)

Variables defined in earlier canvases are available in later ones.


Full Working Example

json
{
  "type": "mathCanvas3",
  "attrs": {
    "canvasData": {
      "autoLayout": true,
      "regions": [
        {
          "id": "title-1",
          "position": { "left": 0, "top": 0 },
          "content": "<b>Beam Calculation</b>",
          "type": "text",
          "boundingBox": { "width": 300, "height": 36 }
        },
        {
          "id": "def-L",
          "position": { "left": 0, "top": 0 },
          "content": "\\placeholder[var]{L}\\coloneq\\placeholder[value]{6\\operatorname{\\textcolor{blue}{m}}}",
          "type": "math"
        },
        {
          "id": "def-q",
          "position": { "left": 0, "top": 0 },
          "content": "\\placeholder[var]{q}\\coloneq\\placeholder[value]{10\\frac{\\operatorname{\\textcolor{blue}{kN}}}{\\operatorname{\\textcolor{blue}{m}}}}",
          "type": "math"
        },
        {
          "id": "calc-M",
          "position": { "left": 0, "top": 0 },
          "content": "\\placeholder[var]{M_{max}}\\coloneq\\placeholder[expr]{\\frac{q\\cdot L^{2}}{8}}=\\placeholder[result]{}",
          "type": "math"
        }
      ]
    },
    "showGrid": true,
    "showBorder": true
  }
}

This creates a canvas with:

  • A bold title "Beam Calculation"
  • L := 6 m
  • q := 10 kN/m
  • M_max := q times L squared / 8 = ? (auto-evaluated)

All regions are auto-arranged vertically on render.

MCP & AI Agents

There are two ways to connect AI assistants to your AltoDocs documents:

ApproachForHow it works
MCP serverCode editors (VS Code Copilot, Claude Desktop, Cursor)Wraps the REST API into Model Context Protocol tools — the editor calls the API on your behalf
OpenClaw SkillOpenClawA SKILL.md file that teaches the assistant how to call the AltoDocs REST API directly — no MCP needed

MCP Server (Code Editors)

The MCP server wraps the REST API so code-editor agents can list documents, create calculations, evaluate formulas, and write results — all via natural language.

Add this to your editor's MCP config:

json
{
  "servers": {
    "altodocs": {
      "type": "stdio",
      "command": "npx",
      "args": ["altodocs-mcp"],
      "env": {
        "ALTODOCS_API_KEY": "your_api_key_here"
      }
    }
  }
}

Get your API key

  1. Open the AltoDocs app
  2. Go to Settings → API Keys
  3. Click Generate and copy the key (starts with altodocs_)
  4. Paste it into the config above

Client-specific setup

VS Code (GitHub Copilot)

Create .vscode/mcp.json in your workspace:

json
{
  "servers": {
    "altodocs": {
      "type": "stdio",
      "command": "npx",
      "args": ["altodocs-mcp"],
      "env": {
        "ALTODOCS_API_KEY": "your_api_key_here"
      }
    }
  }
}

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

json
{
  "mcpServers": {
    "altodocs": {
      "command": "npx",
      "args": ["altodocs-mcp"],
      "env": {
        "ALTODOCS_API_KEY": "your_api_key_here"
      }
    }
  }
}

Available MCP Tools

Once connected, the AI agent has access to these tools:

Documents

ToolDescription
list_documentsList your documents (optionally filter by project)
get_documentRead full document content as JSON
search_documentSearch for text within a document
replace_documentReplace entire document content

Blocks

ToolDescription
list_nodesList all blocks with IDs, types, and text previews
get_nodeGet a specific block by ID
insert_contentInsert new blocks at a position
update_nodeUpdate a block's text or attributes
delete_nodeDelete a block

Calculation Canvases

ToolDescription
list_math_canvasesList all calculation canvases in a document
get_math_canvasGet canvas data with all regions
summarize_math_canvasGet variables, dependencies, and expressions
update_math_canvasUpdate canvas settings or replace all regions
add_math_canvas_regionsAdd new math/text/LaTeX regions
update_math_canvas_regionUpdate a single region (content, position, units)
delete_math_canvas_regionDelete a region
evaluate_math_canvasEvaluate all formulas and optionally write results back

Beam Analyses

ToolDescription
list_beam_analysesList all beam analysis nodes in a document
get_beam_analysisGet beam config, supports, loads, and results
summarize_beam_analysisGet detailed breakdown (support/load types, load cases)
update_beam_analysisUpdate beam config (length, params, units, supports, loads)
add_beam_supportsAdd supports to a beam
update_beam_supportUpdate a specific support
delete_beam_supportDelete a support
add_beam_loadsAdd loads to a beam
update_beam_loadUpdate a specific load
delete_beam_loadDelete a load
run_beam_analysisRun analysis and get reactions/forces/deflection

Screenshots

ToolDescription
list_screenshotsList all screenshot nodes in a document
get_screenshotGet screenshot metadata (caption, alignment, annotation counts)
summarize_screenshotGet extended summary (calibration status, image status, totals)
update_screenshotUpdate caption, alignment, border, dimension settings
get_screenshot_annotationsGet full annotation data (lines, dimensions, shapes, etc.)
update_screenshot_annotationsReplace all annotations
clear_screenshot_annotationsRemove all annotations
set_screenshot_calibrationSet pixel-to-real-world calibration
clear_screenshot_calibrationRemove calibration

OpenClaw Skill (no MCP needed)

OpenClaw does not use MCP. Instead it uses Skills — a SKILL.md file that teaches the assistant about the AltoDocs API, calculation syntax, and unit handling. The assistant then calls the REST API directly.

Quick install

Download SKILL.md from Settings → API Keys (the SKILL.md button next to your key) and place it in your OpenClaw skills folder:

~/.openclaw/skills/altodocs/SKILL.md

Or use the terminal:

bash
mkdir -p ~/.openclaw/skills/altodocs
curl -o ~/.openclaw/skills/altodocs/SKILL.md \
  https://statikdokai.web.app/downloads/altodocs-openclaw-skill/SKILL.md

The skill teaches your OpenClaw assistant:

  • How to create and evaluate engineering calculations
  • The placeholder syntax for math expressions
  • Unit handling and conversion
  • Layout and positioning of canvas regions
  • Beam analysis configuration (supports, loads, running analysis)
  • Screenshot annotation management (lines, dimensions, rectangles, calibration)

Full reference: See OpenClaw Skill for the complete guide — file structure, frontmatter fields, capabilities breakdown, and example conversations.


Example Conversation

Once connected (via MCP or the OpenClaw skill), you can talk to your AI assistant naturally:

You: Create a beam calculation with L = 6 m, q = 10 kN/m, and compute M_max = q*L^2/8

Agent: I'll create a calculation canvas in your document with those parameters... (uses add_math_canvas_regions + evaluate_math_canvas)

Result: M_max = 45.00 kN·m

You: Show delta_max in mm with no decimal places

Agent: I'll update the display settings for delta_max... (uses update_math_canvas_region with unitMemory)

Result: delta_max = 57 mm


Environment Variables

VariableRequiredDescription
ALTODOCS_API_KEYYesYour API key (generate in Settings → API Keys)
ALTODOCS_API_URLNoCustom API base URL (defaults to production)

Full SKILL.md Reference

Below is the complete SKILL.md file. You can copy this directly instead of downloading:

<details> <summary><strong>Click to expand full SKILL.md</strong></summary>
markdown
---
name: altodocs
description: Control AltoDocs engineering documents — create calculations, evaluate structural formulas, manage beam analyses, annotate images, and read/write document content via the AltoDocs REST API.
metadata: { "openclaw": { "emoji": "📐", "requires": { "env": ["ALTODOCS_API_KEY"] }, "primaryEnv": "ALTODOCS_API_KEY", "homepage": "https://altodocs.io" } }
---

# AltoDocs — Engineering Document API

You have access to the AltoDocs API for creating and managing structural engineering documents with live calculation canvases, beam analyses, and annotated images.

## API Base URL

```
https://europe-west1-statikdokai.cloudfunctions.net/api
```

All requests need `Authorization: Bearer $ALTODOCS_API_KEY`.

## Core Workflow

1. **List documents**: `GET /documents`
2. **Get a document**: `GET /documents/:id` — returns JSON with all blocks (headings, paragraphs, calculation canvases, beam analyses, screenshots)
3. **Create calculations**: `POST /documents/:id/math-canvases/:nodeId/regions` — add math regions to a canvas
4. **Evaluate**: `POST /documents/:id/math-canvases/:nodeId/evaluate` with `{"writeBack": true}` — computes all formulas and writes results back
5. **Change units/precision**: `PUT /documents/:id/math-canvases/:nodeId/regions/:regionId` with `{"unitMemory": {"preferredUnit": "mm", "precision": 0}}`

## Available Endpoints

### Documents & Blocks

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/documents` | List all documents |
| POST | `/documents` | Create a document |
| GET | `/documents/:id` | Get document as JSON |
| PUT | `/documents/:id` | Replace entire document |
| GET | `/documents/:id/search?q=keyword` | Search text |
| GET | `/documents/:id/nodes` | List all blocks |
| GET | `/documents/:id/nodes/:nodeId` | Get a block |
| POST | `/documents/:id/nodes` | Insert blocks |
| PUT | `/documents/:id/nodes/:nodeId` | Update a block |
| DELETE | `/documents/:id/nodes/:nodeId` | Delete a block |

### Calculation Canvases (mathCanvas3)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/documents/:id/math-canvases` | List canvases |
| GET | `/documents/:id/math-canvases/:nodeId` | Get canvas data |
| GET | `/documents/:id/math-canvases/:nodeId/summary` | Get variables, dependencies |
| PUT | `/documents/:id/math-canvases/:nodeId` | Update canvas attrs |
| POST | `/documents/:id/math-canvases/:nodeId/regions` | Add regions |
| PUT | `/documents/:id/math-canvases/:nodeId/regions/:regionId` | Update a region |
| DELETE | `/documents/:id/math-canvases/:nodeId/regions/:regionId` | Delete a region |
| POST | `/documents/:id/math-canvases/:nodeId/evaluate` | Evaluate all math |

### Beam Analyses (beamAnalysis)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/documents/:id/beam-analyses` | List beam analysis nodes |
| GET | `/documents/:id/beam-analyses/:nodeId` | Get beam config & results |
| GET | `/documents/:id/beam-analyses/:nodeId/summary` | Get detailed breakdown |
| PUT | `/documents/:id/beam-analyses/:nodeId` | Update beam config |
| POST | `/documents/:id/beam-analyses/:nodeId/supports` | Add supports |
| PUT | `/documents/:id/beam-analyses/:nodeId/supports/:supportId` | Update a support |
| DELETE | `/documents/:id/beam-analyses/:nodeId/supports/:supportId` | Delete a support |
| POST | `/documents/:id/beam-analyses/:nodeId/loads` | Add loads |
| PUT | `/documents/:id/beam-analyses/:nodeId/loads/:loadId` | Update a load |
| DELETE | `/documents/:id/beam-analyses/:nodeId/loads/:loadId` | Delete a load |
| POST | `/documents/:id/beam-analyses/:nodeId/analyze` | Run analysis |

### Screenshots (screenshot)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/documents/:id/screenshots` | List screenshot nodes |
| GET | `/documents/:id/screenshots/:nodeId` | Get screenshot metadata |
| GET | `/documents/:id/screenshots/:nodeId/summary` | Get extended summary |
| PUT | `/documents/:id/screenshots/:nodeId` | Update attrs (caption, alignment, etc.) |
| GET | `/documents/:id/screenshots/:nodeId/annotations` | Get annotation data |
| PUT | `/documents/:id/screenshots/:nodeId/annotations` | Replace annotations |
| DELETE | `/documents/:id/screenshots/:nodeId/annotations` | Clear annotations |
| PUT | `/documents/:id/screenshots/:nodeId/calibration` | Set calibration |
| DELETE | `/documents/:id/screenshots/:nodeId/calibration` | Clear calibration |

---

## Math Syntax

Calculations use LaTeX with a placeholder system. There are exactly 4 placeholder types:

- `\placeholder[var]{NAME}` — variable name (e.g. `L`, `M_{max}`, `f_{ck}`)
- `\placeholder[value]{NUMBER UNIT}` — numeric value with unit
- `\placeholder[expr]{EXPRESSION}` — formula referencing other variables
- `\placeholder[result]{}` — computed answer (leave empty, system fills it)

### Defining a constant

```
\placeholder[var]{L}\coloneq\placeholder[value]{6\operatorname{\textcolor{blue}{m}}}
```
This defines `L := 6 m`.

### Defining a computed variable

```
\placeholder[var]{M_{max}}\coloneq\placeholder[expr]{\frac{q\cdot L^{2}}{8}}=\placeholder[result]{}
```
This defines `M_max := q·L²/8 = ?` — the result is auto-computed.

**IMPORTANT:** The `=\placeholder[result]{}` at the end triggers evaluation. Without it, nothing computes. Always leave the result placeholder empty.

### Assignment operator

Always use `\coloneq` (renders as `:=`). Never use `:=` directly.

## Units

Units go inline inside `value` or `result` placeholders:

```
\operatorname{\textcolor{blue}{kN}}
```

Common units: `mm`, `cm`, `m`, `km`, `N`, `kN`, `MN`, `Pa`, `kPa`, `MPa`, `GPa`, `kg`, `ton`, `deg`, `rad`

Compound: `\frac{\operatorname{\textcolor{blue}{kN}}}{\operatorname{\textcolor{blue}{m}}}` for kN/m

## Unit Conversion

Set `unitMemory` on a region to convert display unit or change precision:

```json
{
  "unitMemory": {
    "preferredUnit": "mm",
    "precision": 0,
    "numberFormat": "decimal"
  }
}
```

After updating unitMemory, re-evaluate with `writeBack: true` to apply.

## LaTeX Operations

| Operation | LaTeX |
|-----------|-------|
| Multiply | `\cdot` |
| Fraction | `\frac{a}{b}` |
| Power | `^{2}` |
| Square root | `\sqrt{x}` |
| Parentheses | `\left( \right)` |
| Subscript | `_{max}` |
| Greek | `\alpha`, `\sigma`, `\delta`, `\pi` |

## Region Types

Each canvas contains regions of three types:
- `"math"` — evaluatable math expressions
- `"text"` — HTML text labels (e.g. `<b>Input Parameters</b>`)
- `"latex"` — display-only LaTeX (not evaluated)

## Positioning

Regions sit on a 2D canvas with `position: {left, top}` in pixels. Evaluation order is top-to-bottom, then left-to-right. Variables must be defined above the expressions that use them.

Use `autoLayout: true` in `canvasData` to let the app stack regions vertically.

## Example: Create a Beam Calculation

```bash
curl -X POST "$API/documents/$DOC/math-canvases/$CANVAS/regions" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "regions": [
      {
        "id": "title-1",
        "position": {"left": 0, "top": 0},
        "content": "<b>Simply Supported Beam</b>",
        "type": "text",
        "boundingBox": {"width": 300, "height": 36}
      },
      {
        "id": "def-L",
        "position": {"left": 0, "top": 0},
        "content": "\\placeholder[var]{L}\\coloneq\\placeholder[value]{6\\operatorname{\\textcolor{blue}{m}}}",
        "type": "math"
      },
      {
        "id": "def-q",
        "position": {"left": 0, "top": 0},
        "content": "\\placeholder[var]{q}\\coloneq\\placeholder[value]{10\\frac{\\operatorname{\\textcolor{blue}{kN}}}{\\operatorname{\\textcolor{blue}{m}}}}",
        "type": "math"
      },
      {
        "id": "calc-M",
        "position": {"left": 0, "top": 0},
        "content": "\\placeholder[var]{M_{max}}\\coloneq\\placeholder[expr]{\\frac{q\\cdot L^{2}}{8}}=\\placeholder[result]{}",
        "type": "math"
      }
    ]
  }'
```

This creates: L := 6 m, q := 10 kN/m, M_max := q·L²/8 = 45.00 kN·m

---

## Beam Analysis

Beam analysis nodes model simply-supported or continuous beams with supports, loads, and material properties.

### Beam Config

```json
{
  "beam_length": 6000,
  "beam_params": { "E": 210000, "I": 8360000 },
  "units": { "length": "mm", "force": "kN", "distributed": "kN/m", "moment": "kN.m", "E": "MPa", "I": "mm4", "deflection": "mm" },
  "supports": [
    { "location": 0, "type": "pin" },
    { "location": 6000, "type": "roller" }
  ],
  "loads": [
    { "type": "UDLV", "magnitude": -10, "start_location": 0, "end_location": 6000 }
  ]
}
```

### Support types
`"pin"`, `"roller"`, `"fixed"`, `"moment_roller"`, `"y_spring"` (spring requires `ky` stiffness)

### Load types
- `"point"` — point load (`location`, `magnitude`)
- `"moment"` — applied moment (`location`, `magnitude`)
- `"UDLV"` — uniform distributed load (`start_location`, `end_location`, `magnitude`)
- `"TrapezoidalLoadV"` — trapezoidal load (`start_location`, `end_location`, `magnitude`, `magnitude_end`)

All loads support optional `loadCase` (string) and `factor` (number, default 1.0).

### Run analysis

```bash
curl -X POST "$API/documents/$DOC/beam-analyses/$NODE/analyze" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"writeBack": true}'
```

Returns reactions, shear/moment/deflection data. With `writeBack: true`, results are stored in the document and visible instantly in the browser.

### Example: Configure a simply supported beam

```bash
curl -X PUT "$API/documents/$DOC/beam-analyses/$NODE" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "beam_length": 6000,
    "beam_params": { "E": 210000, "I": 8360000 },
    "supports": [
      { "location": 0, "type": "pin" },
      { "location": 6000, "type": "roller" }
    ],
    "loads": [
      { "type": "UDLV", "magnitude": -10, "start_location": 0, "end_location": 6000 }
    ]
  }'
```

---

## Screenshots & Annotations

Screenshot nodes are embedded images with an annotation layer (lines, dimensions, rectangles, circles, text boxes, leader labels).

### Reading screenshots

```bash
# List all screenshots in a document
curl "$API/documents/$DOC/screenshots" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY"

# Get summary with annotation counts
curl "$API/documents/$DOC/screenshots/$NODE/summary" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY"
```

The summary includes: `annotationCounts` (per shape type), `totalAnnotations`, `isCalibrated`, `imageStatus` (`"uploaded"` / `"local"` / `"missing"`).

### Updating screenshot metadata

```bash
curl -X PUT "$API/documents/$DOC/screenshots/$NODE" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Figure 3 — Foundation plan",
    "alignment": "center",
    "showBorder": true,
    "dimensionUnit": "mm",
    "dimensionDecimals": 0
  }'
```

Updatable fields: `alt`, `title`, `caption`, `alignment` (`"left"` / `"center"` / `"right"`), `captionAlignment`, `width`, `height`, `showBorder`, `defaultStrokeStyle` (`"solid"` / `"dashed"` / `"dotted"`), `dimensionScale`, `dimensionUnit` (`"mm"` / `"cm"` / `"m"`), `dimensionDecimals`, `calibration`.

### Calibration

Maps pixel distances to real-world measurements for accurate dimension annotations:

```bash
curl -X PUT "$API/documents/$DOC/screenshots/$NODE/calibration" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"px": 200, "mm": 1000, "scale": 5}'
```

If a known 1-meter wall is 200 pixels: `scale = mm / px = 5`. A 100px dimension then displays as "500 mm".

### Annotations

Annotations are geometric shapes drawn on the image. Get them:

```bash
curl "$API/documents/$DOC/screenshots/$NODE/annotations" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY"
```

Replace all annotations:

```bash
curl -X PUT "$API/documents/$DOC/screenshots/$NODE/annotations" \
  -H "Authorization: Bearer $ALTODOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {"id": "l1", "startX": 100, "startY": 200, "endX": 400, "endY": 200, "color": "#ff0000", "width": 2}
    ],
    "dimensions": [
      {"id": "d1", "startX": 50, "startY": 400, "endX": 450, "endY": 400, "color": "#0000ff", "width": 2, "text": "", "fontSize": 14, "auto": true}
    ],
    "rectangles": [],
    "circles": [],
    "textBoxes": [
      {"id": "t1", "x": 100, "y": 50, "w": 200, "text": "Column C1", "fontSize": 16, "color": "#000000"}
    ],
    "leaders": []
  }'
```

#### Annotation shape types

- **Line**: `{id, startX, startY, endX, endY, color, width, isArrow?, strokeStyle?}`
- **Dimension**: `{id, startX, startY, endX, endY, color, width, text, fontSize, auto?, strokeStyle?}` — with `auto: true`, text is computed from calibration
- **Rectangle**: `{id, startX, startY, endX, endY, color, width, strokeStyle?, fillStyle?, fillColor?, fillOpacity?}`
- **Circle**: `{id, centerX, centerY, radius, color, width, strokeStyle?, fillStyle?, fillColor?, fillOpacity?}`
- **Text Box**: `{id, x, y, w, h?, text, fontSize, color}`
- **Leader**: `{id, tipX, tipY, kneeX, kneeY, textX, textY, text, fontSize, color, width, strokeStyle?}`

Stroke styles: `"solid"`, `"dashed"`, `"dotted"`. Fill styles: `"none"`, `"solid"`, `"hatch"`, `"crosshatch"`, `"dots"`, `"grid"`.

---

## Tips

- Always use `autoLayout: true` for new canvases — the app will stack regions neatly
- Use `GET /documents/:id/math-canvases/:nodeId/summary` to quickly see all defined variables and dependencies
- After adding regions, the server auto-evaluates and returns results — check the `evaluation` field in the response
- Results appear instantly in any open browser via live sync
- Only `var`, `value`, `expr`, `result` are valid placeholder types — anything else shows red error squares
- For beam analyses, use `/summary` to get support/load type breakdowns before modifying
- Screenshot annotations reference pixel coordinates on the original image — use calibration to map to real-world units
</details>