@rui.branco/revit-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +496 -0
- package/index.js +124 -0
- package/lib/bridge.js +161 -0
- package/lib/tools/detail.js +86 -0
- package/lib/tools/diagnostics.js +57 -0
- package/lib/tools/document.js +149 -0
- package/lib/tools/documentation.js +550 -0
- package/lib/tools/geometry.js +331 -0
- package/lib/tools/graphics.js +197 -0
- package/lib/tools/install.js +208 -0
- package/lib/tools/material-appearance.js +168 -0
- package/lib/tools/materials.js +247 -0
- package/lib/tools/model.js +285 -0
- package/lib/tools/parameters.js +140 -0
- package/lib/tools/quality.js +200 -0
- package/lib/tools/read.js +142 -0
- package/lib/tools/reload.js +27 -0
- package/lib/tools/sheet-collections.js +62 -0
- package/lib/tools/sheets.js +76 -0
- package/lib/tools/titleblocks.js +152 -0
- package/lib/tools/views.js +746 -0
- package/lib/tools/write.js +122 -0
- package/manifest.json +416 -0
- package/package.json +52 -0
- package/revit-bridge/README.md +1256 -0
- package/revit-bridge/dist/RevitMcpBridge.Handlers.deps.json +23 -0
- package/revit-bridge/dist/RevitMcpBridge.Handlers.dll +0 -0
- package/revit-bridge/dist/RevitMcpBridge.deps.json +23 -0
- package/revit-bridge/dist/RevitMcpBridge.dll +0 -0
- package/revit-bridge/install.ps1 +676 -0
|
@@ -0,0 +1,1256 @@
|
|
|
1
|
+
# Revit MCP Bridge
|
|
2
|
+
|
|
3
|
+
A Revit add-in that runs a small HTTP server **inside Revit's own process** and dispatches requests
|
|
4
|
+
to the Revit API. It is the Revit half of `revit-mcp`: the Node MCP server talks to it over
|
|
5
|
+
loopback HTTP, and this add-in does the part that can only happen inside Revit.
|
|
6
|
+
|
|
7
|
+
- Endpoint base: `http://127.0.0.1:48884/revit-mcp/`
|
|
8
|
+
- Target: Revit **2025, 2026, 2027** (`net8.0-windows`, x64)
|
|
9
|
+
- Runtime dependencies: none beyond Revit itself and the in-box .NET base class library
|
|
10
|
+
(`System.Net.HttpListener`, `System.Text.Json`)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
One command, from this directory. No Visual Studio, no admin rights, **no .NET SDK**:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
.\install.ps1
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That detects every installed Revit, copies the add-in to
|
|
23
|
+
`%APPDATA%\Autodesk\Revit\Addins\<version>\RevitMcpBridge\`, and writes the `.addin` manifest next
|
|
24
|
+
to it. Then **restart Revit to load the bridge**.
|
|
25
|
+
|
|
26
|
+
The add-in comes from `dist\` — the prebuilt DLL the npm package ships, which is why an end user
|
|
27
|
+
needs neither the SDK nor a build step. One binary covers 2025, 2026 and 2027 (see
|
|
28
|
+
[Build](#build)). Only a source checkout with no `dist\` falls through to `dotnet build`, and if
|
|
29
|
+
the SDK is missing there too, the installer says so and names both ways out rather than failing
|
|
30
|
+
obscurely.
|
|
31
|
+
|
|
32
|
+
| Flag | Effect |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `-RevitVersion 2026` | Only that version. Works even if Revit is not in the default Program Files location. |
|
|
35
|
+
| `-Build` | Force a fresh `dotnet build -c Release` and install that instead of `dist\`. Needs the SDK; the contributor path. |
|
|
36
|
+
| `-SkipBuild` | Never build. Already the default whenever `dist\` is present, so it only matters in a checkout without one. |
|
|
37
|
+
| `-Uninstall` | Remove the manifest and the install folder. |
|
|
38
|
+
| `-Json` | Emit a single JSON result object as the **only** stdout output. |
|
|
39
|
+
|
|
40
|
+
Re-running is safe. The install folder is deleted and recreated rather than merged into, so a file
|
|
41
|
+
that disappeared from a newer build cannot linger.
|
|
42
|
+
|
|
43
|
+
### Uninstall
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
.\install.ps1 -Uninstall
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Uninstall also cleans versions whose Revit has since been removed but whose add-ins folder is still
|
|
50
|
+
there, so it can finish the job after Revit is gone.
|
|
51
|
+
|
|
52
|
+
### Calling the installer from a tool
|
|
53
|
+
|
|
54
|
+
Every human-readable line is prefixed and greppable:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
[revit-bridge] INFO action=install addin=RevitMcpBridge minimumRevit=2025
|
|
58
|
+
[revit-bridge] DETECTED version=2026 hasExe=True path="C:\Program Files\Autodesk\Revit 2026"
|
|
59
|
+
[revit-bridge] BUILD status=skipped reason=prebuilt-dist
|
|
60
|
+
[revit-bridge] OUTPUT dir="...\revit-bridge\dist"
|
|
61
|
+
[revit-bridge] INSTALLED version=2026 files=2 dir="..." manifest="..."
|
|
62
|
+
[revit-bridge] RESULT status=ok action=install versions=2026
|
|
63
|
+
[revit-bridge] DONE Restart Revit to load the bridge
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Levels are `INFO`, `DETECTED`, `BUILD`, `OUTPUT`, `INSTALLED`, `REMOVED`, `RESULT`, `DONE`, `WARN`,
|
|
67
|
+
`ERROR`.
|
|
68
|
+
|
|
69
|
+
For programmatic use prefer `-Json`, which suppresses all of the above and prints one object:
|
|
70
|
+
|
|
71
|
+
```jsonc
|
|
72
|
+
{
|
|
73
|
+
"ok": true,
|
|
74
|
+
"action": "install",
|
|
75
|
+
"exitCode": 0,
|
|
76
|
+
"message": "Restart Revit to load the bridge",
|
|
77
|
+
"addInName": "RevitMcpBridge",
|
|
78
|
+
"addInId": "8f2c7a41-3e6b-4d19-9c05-1b7a52e4d83f",
|
|
79
|
+
"fullClassName": "RevitMcpBridge.BridgeApplication",
|
|
80
|
+
"vendorId": "com.github.revit-mcp",
|
|
81
|
+
"buildStatus": "prebuilt",
|
|
82
|
+
"outputDir": "...",
|
|
83
|
+
"versions": [ { "version": "2026", "installDir": "...", "manifest": "...", "status": "installed" } ],
|
|
84
|
+
"warnings": [],
|
|
85
|
+
"errors": [],
|
|
86
|
+
"log": [ "[revit-bridge] INFO ..." ]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Exit codes:
|
|
91
|
+
|
|
92
|
+
| Code | Meaning |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| 0 | Success |
|
|
95
|
+
| 1 | Unhandled failure |
|
|
96
|
+
| 2 | No Revit installation detected |
|
|
97
|
+
| 3 | Requested version unsupported (2024 or earlier), or bad `-RevitVersion` |
|
|
98
|
+
| 4 | `dotnet build` failed, or the .NET SDK is missing and there was no `dist\` to fall back on |
|
|
99
|
+
| 5 | Nothing to install: no `dist\` and no Release build output |
|
|
100
|
+
|
|
101
|
+
The installer runs under **Windows PowerShell 5.1** as well as PowerShell 7 — no ternary, no `??`,
|
|
102
|
+
no `&&`/`||`, no `-AsHashtable`.
|
|
103
|
+
|
|
104
|
+
> **Revit 2024 and earlier are refused.** They host add-ins on .NET Framework 4.8 and physically
|
|
105
|
+
> cannot load a `net8.0-windows` assembly. The installer warns and exits non-zero rather than
|
|
106
|
+
> installing something that would fail inside Revit with a confusing load error.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Build
|
|
111
|
+
|
|
112
|
+
Only contributors need this; installing does not.
|
|
113
|
+
|
|
114
|
+
```powershell
|
|
115
|
+
dotnet build -c Release
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
From the repo root, `npm run build:bridge` does the same and then refreshes `dist\` from the
|
|
119
|
+
output, which is the only supported way to update the binary that ships on npm.
|
|
120
|
+
|
|
121
|
+
**Revit does not need to be installed to build this.** The Revit API assemblies come from a
|
|
122
|
+
community reference package:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Revit_All_Main_Versions_API_x64 2025.0.0 (ships lib/net8.0/RevitAPI.dll + RevitAPIUI.dll)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Two things about that reference are deliberate and should not be "cleaned up":
|
|
129
|
+
|
|
130
|
+
1. **It is pinned to 2025, the lowest supported release.** An add-in compiled against 2025 loads
|
|
131
|
+
fine in 2026 and 2027; one compiled against 2027 would break on 2025. Bumping this pin silently
|
|
132
|
+
drops support for older Revit versions.
|
|
133
|
+
2. **`ExcludeAssets=runtime` / `Private=false`.** The API assemblies are reference-only and must
|
|
134
|
+
never be copied next to the add-in — Revit supplies them at runtime, and shipping a second copy
|
|
135
|
+
makes every type identity check fail. A post-build MSBuild target (`AssertNoRevitApiInOutput`)
|
|
136
|
+
fails the build if one ever leaks into the output folder, and `install.ps1` refuses to copy them
|
|
137
|
+
as a second line of defence.
|
|
138
|
+
|
|
139
|
+
The build output is the two assemblies — `RevitMcpBridge.dll` and `RevitMcpBridge.Handlers.dll` —
|
|
140
|
+
with a `.pdb` and a `.deps.json` each. See [Building both halves](#building-both-halves) for why one
|
|
141
|
+
`dotnet build` produces both, and [Hot reload](#hot-reload) for why there are two at all.
|
|
142
|
+
|
|
143
|
+
### `dist\` — the binary that ships
|
|
144
|
+
|
|
145
|
+
`dist\` holds both DLLs and both `.deps.json` files copied out of `bin\Release`, and
|
|
146
|
+
it is **committed to git on purpose**: it is what lets the npm package install without a .NET SDK.
|
|
147
|
+
The `.pdb` is left behind — it is debug weight nobody installing from npm can use. `bin\` and
|
|
148
|
+
`obj\` stay ignored; `dist\` must not be. `prepublishOnly` re-runs `build:bridge` and the test
|
|
149
|
+
suite so a stale or broken binary cannot be published.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Endpoints
|
|
154
|
+
|
|
155
|
+
Everything is **POST** with a JSON body, under `http://127.0.0.1:48884/revit-mcp/`. Reads carry
|
|
156
|
+
their filters in the body; there are no GET endpoints and no query strings. An empty body is
|
|
157
|
+
treated as `{}`.
|
|
158
|
+
|
|
159
|
+
| Endpoint | Request | Response |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `status` | `{}` | `{bridgeVersion, url, units, revitVersion, revitVersionName, revitVersionBuild, revitSubVersion, username, activeDocument}` — `activeDocument` is `null` when no document is open, otherwise `{title, pathName, isWorkshared, isFamilyDocument, isReadOnly, isModified}` |
|
|
162
|
+
| `levels` | `{}` | `[{id, name, elevation}]` |
|
|
163
|
+
| `categories` | `{}` | `[{category, count}]`, busiest first |
|
|
164
|
+
| `query` | `{category?, level?, typeName?, limit?, offset?}` | `{total, offset, limit, rows: [compact]}` |
|
|
165
|
+
| `elements` | `{ids: [...], params?: ["Comments", ...]}` | `[compact + {found, parameters?}]` |
|
|
166
|
+
| `selection` | `{}` | `{count, ids: [...], elements: [compact]}` |
|
|
167
|
+
| `titleblocks` | `{}` | `[{id, familyName, typeName}]` — an empty array when no title block family is loaded |
|
|
168
|
+
| `sheets` | `{}` | `[{id, number, name}]`, by sheet number |
|
|
169
|
+
| `levels/create` | `[{name?, elevation}]` or `{levels: [...]}` | `[{id, name, elevation}]` |
|
|
170
|
+
| `walls/create` | `{level, wallType?, typeName?, height, curves: [{start:{x,y}, end:{x,y}}]}` | `[{id, name, typeName, level, height}]` |
|
|
171
|
+
| `parameters/set` | `{ids: [...], name, value}` | `{updated, results: [{id, name, value, display}]}` |
|
|
172
|
+
| `parameters/create-project` | `{name, category \| categories: [...], type?, group?, instance?}` | `{name, guid, categories, instance, created, alreadyExisted}` |
|
|
173
|
+
| `sheets/set-parameter` | `{values: [{sheetId, name, value}]}` | `{updated: [{sheetId, number, name, value, display}], failed: [{sheetId, name, code, reason}]}` |
|
|
174
|
+
| `elements/delete` | `{ids: [...]}` | `{requested, deleted, deletedIds: [...]}` |
|
|
175
|
+
| `sheets/create` | `{titleBlockId?, sheets: [{number, name}]}` | `{created: [{id, number, name}], skipped: [{number, reason}]}` |
|
|
176
|
+
| `toposolid/create` | `{points: [{x, y, z}], typeName?, level?}` | `{id, type, typeName, level, points}` — `type` is `"Toposolid"` or `"TopographySurface"` |
|
|
177
|
+
| `toposolid/flatten` | `{points: [{x, y}], elevation, toposolidId?}` | `{id, elevation, added, creases, flattened, offsetMode, residual}` |
|
|
178
|
+
| `floors/create` | `{level, typeName?, boundary: [{x, y}], structural?, offset?}` | `{id, level, typeName, structural, offset, area}` |
|
|
179
|
+
| `families/load` | `{paths: [string]}` or `{path: string}` | `{families: [{path, familyName, loaded, alreadyLoaded, symbols: [{id, typeName}], code?, reason?}], warnings: [...]}` |
|
|
180
|
+
| `families/symbols` | `{category?, familyName?}` | `[{id, familyName, typeName, category}]` — an empty array when nothing has been loaded into the project |
|
|
181
|
+
| `families/place` | `{symbolId, level?, z?, points: [{x, y, z?}], rotation?}` | `{symbolId, familyName, typeName, level, levelElevation, placed: [{id, x, y, z, placedZ}], failed: [{x, y, reason}]}` |
|
|
182
|
+
| `openings/place` | `{symbolId, points: [{x, y, z?}], hostWallId?, level?, sillHeight?}` | `{symbolId, familyName, typeName, category, level, levelElevation, placed: [{id, x, y, z, hostWallId, hostWallType, sillHeight, sillHeightOn?, placedZ}], failed: [{x, y, z, code, reason}]}` — `code` is `NO_HOST_WALL` or `REVIT_API_ERROR` |
|
|
183
|
+
| `views` | `{}` | `[{id, name, viewType, isTemplate, isPlacedOnSheet, viewDirection?, rightDirection?, upDirection?}]` — non-template views, sheets excluded |
|
|
184
|
+
| `views/create-plan` | `{level, name, viewFamilyType?, scale?}` | `{id, name, viewType, viewFamilyType, level, scale}` |
|
|
185
|
+
| `views/create-drafting` | `{name, scale?}` | `{id, name, viewType, scale}` |
|
|
186
|
+
| `views/create-section` | `{name, origin: {x, y, z}, direction: {x, y}, width, height, depth, scale?}` | `{id, name, viewType, scale, viewDirection, rightDirection, upDirection}` |
|
|
187
|
+
| `views/create-3d` | `{name, eye?: {x, y, z}, target?: {x, y, z}, perspective?, scale?}` | `{id, name, viewType, isPerspective, scale, viewDirection, rightDirection, upDirection, modelExtents: {min, max, center} \| null}` |
|
|
188
|
+
| `views/set-style` | `{viewId, style, detailLevel?}` | `{viewId, name, viewType, style, detailLevel}` — `shadows` is redirected with `SHADOWS_HANDLED_ELSEWHERE` to `views/set-graphics` |
|
|
189
|
+
| `views/set-background` | `{viewId, kind, skyColor?, horizonColor?, groundColor?, imagePath?}` | `{viewId, name, viewType, kind, skyColor?, horizonColor?, groundColor?, imagePath?, imageFlags?}` — 3D views only; `kind: "sky"` reads back as `SunAndClouds` and takes no colours |
|
|
190
|
+
| `views/hide-categories` | `{viewId, categories: [string], hidden?}` | `{viewId, name, viewType, categories: [{category, hidden, skipped?, reason?}]}` — `"annotation"` expands to the whole datum/annotation set |
|
|
191
|
+
| `views/set-sun` | `{viewId, azimuth?, altitude?}` in degrees, or `{viewId, date?, time?}` | `{viewId, name, viewType, sunSettingsId, sharesSettings, sunAndShadowType, relativeToView, azimuth, altitude, dateAndTime}` — angles read back, not echoed |
|
|
192
|
+
| `views/export-image` | `{viewId \| viewIds, path?, width?, height?, format?}` | single: `{viewId, viewName, path, requestedPath, bytes, width, height}`; batch: `{exported: [...]}` |
|
|
193
|
+
| `views/legends` | `{}` | `[{id, name, scale}]` — the legend views the document already has, by name |
|
|
194
|
+
| `views/create-legend` | `{name, fromLegendId?, scale?}` | `{id, name, viewType, sourceViewId, scale}`, or `NO_LEGEND_TO_DUPLICATE` when the document has no legend |
|
|
195
|
+
| `views/duplicate` | `{viewId, name, detailing?}` | `{id, name, viewType, sourceViewId, detailing}` |
|
|
196
|
+
| `views/set-scale` | `{viewId, scale}` or `{viewIds: [...], scale}` | `{updated: [{id, name, viewType, scale}], failed: [{viewId, code, reason}]}` — a perspective view fails with `PERSPECTIVE_VIEW_HAS_NO_SCALE` pointing at `views/scale-perspective-crop` |
|
|
197
|
+
| `views/scale-perspective-crop` | `{viewId, multiplier, dryRun?}` | `{dryRun, applied, viewId, name, viewType, multiplier, before, after, cameraUnchanged, note}` — `View3D.ScalePerspectiveCropBox`; `dryRun` defaults to **true** and reports no predicted `after`; `before`/`after` are `{isPerspective, scale, outline, cropBoxActive, cropBox, camera, viewport}` measured after a regeneration, `outline` and `viewport` in PAPER feet; refusals are `NOT_A_3D_VIEW`, `VIEW_IS_TEMPLATE`, `VIEW_NOT_PERSPECTIVE` |
|
|
198
|
+
| `sheets/place-view` | `{sheetId, viewId, x?, y?}` or `{placements: [{sheetId, viewId, x?, y?}]}` | single: `{viewportId, sheetId, viewId, kind, x, y}`; batch: `{placed: [...], failed: [{sheetId, viewId, code, reason}]}` |
|
|
199
|
+
| `schedules/create` | `{category, name, fields: [string], scale?}` | `{id, name, category, fields, skippedFields, availableFields?}` |
|
|
200
|
+
| `views/graphics` | `{viewId}` or `{viewIds: [...]}` | `{views: [{id, name, viewType, style, detailLevel, templateId, templateName, shadowIntensity, sunlightIntensity, ambientLightIntensity, background, shadows: <probe>, exposure: <probe>}], notes}` — a probe is `{parameter, parameterId, available, storageType, readOnly, value, display, on, templateId, templateName, controlledByTemplate, writable, writableReason}` and `on: null` means UNKNOWN |
|
|
201
|
+
| `views/set-graphics` | `{viewId \| viewIds, style?, detailLevel?, shadowIntensity?, sunlightIntensity?, shadows?}` | `{views: [...], notes, shadows?}` — `shadows.method` is `parameter` (written and read back, `verified`), `posted-command` (`posted`, `pending`, `unverified`), `unavailable` or `blocked`; `ambientLightIntensity` is refused `409 AMBIENT_LIGHT_NOT_EXPOSED` |
|
|
202
|
+
| `views/graphics-command-status` | `{}` | `{posted, command, requested, viewId, postedAtUtc, idleSeenAtUtc, pending, probeAtPost, probeAtIdle, probeNow, verified, verifiedBy}` |
|
|
203
|
+
| `views/capture-template` | `{sourceViewId, name, mode?: "graphics" \| "shadows" \| "all", parameterIds?: [...]}` | `{templateId, name, viewType, sourceViewId, sourceViewName, mode, controlled: [{id, name}], notControlled, excluded: [{id, name, reason}], ignoredParameterIds, shadows}` |
|
|
204
|
+
| `views/apply-template` | `{templateId, viewIds, mode?: "apply" \| "assign", dryRun?, replace?}` | `{dryRun, applied, templateId, templateName, mode, plan: [...], views?, shadows, note?}` — `dryRun` defaults to **true**; the batch is validated before any write |
|
|
205
|
+
| `detail/lines` | `{viewId, lines: [{start: {x, y}, end: {x, y}}], lineStyle?}` | `{viewId, elevation, lineStyle, created: [{id, start, end}], failed: [{index, reason}], requestedLineStyle?, availableLineStyles?}` |
|
|
206
|
+
| `detail/text` | `{viewId, notes: [{x, y, text, size?}]}` | `{viewId, elevation, created: [{id, x, y, typeName}], failed: [{index, reason}]}` |
|
|
207
|
+
| `directshape/create` | `{category, name?, typeName?, materialId?, materialName?, comments?, mark?, shapes: [primitive \| group]}` | `{created: [{id, category, name, typeId, typeName, materialId?, comments?, mark?}], failed: [{index, reason}]}` |
|
|
208
|
+
| `planting/place` | `{points: [{x, y, z?, name?, comments?, mark?}], trunkHeight?, trunkRadius?, crownRadius?, name?, typeName?, materialId?, materialName?, comments?, mark?}` | `{created: [{id, category, name, typeId, typeName, materialId?, x, y, comments?, mark?}], failed: [{index, reason}]}` |
|
|
209
|
+
| `pipes/create` | `{category?, name?, typeName?, materialId?, materialName?, comments?, mark?, runs: [{points: [{x, y, z?}], radius?, name?, comments?, mark?}]}` | `{created: [{id, category, name, typeId, typeName, materialId?, comments?, mark?}], failed: [{index, reason}]}` |
|
|
210
|
+
| `sprinklers/place` | `{points: [{x, y, z?, name?, comments?, mark?}], radius?, height?, name?, typeName?, materialId?, materialName?, comments?, mark?}` | `{created: [{id, category, name, typeId, typeName, materialId?, x, y, comments?, mark?}], failed: [{index, reason}]}` |
|
|
211
|
+
| `materials` | `{}` | `[{id, name, colorRgb, appearanceAssetId}]`, by name — `colorRgb` is `{r, g, b}` or `null` |
|
|
212
|
+
| `materials/create` | `{name, color: {r, g, b}, transparency?, shininess?, surfaceForegroundPatternId?, appearanceAssetId?, texturePath?}` | `{id, name, created, colorRgb, transparency, shininess, appearanceAssetId, texture?}` |
|
|
213
|
+
| `materials/set-texture` | `{materialId \| materialName, texturePath, scale?: {x, y}, rotation?, tint?: {r, g, b}}` | `{materialId, materialName, assetAuthored, assetReason, appearanceAssetId, appearanceAssetName, assetSchema, colorRgb, texturePath, diffuseProperty, textureApplied, set: {property: value}, missing: [property], verified: {connectedAsset, bitmap, scaleFeet: {x, y}, rotation}}` |
|
|
214
|
+
| `materials/assign` | `{materialId \| materialName, elementIds: [...]}` | `{materialId, materialName, assigned: [{id, route, appliesToType, typeId?, typeName?, layers?, parameter?}], skipped: [{id, reason}]}` |
|
|
215
|
+
| `walltypes/create` | `{name, basedOnTypeName?, thickness?, materialId?, materialName?}` | `{id, name, created, basedOn, thickness, materialId, materialName, layers}` |
|
|
216
|
+
| `floortypes/create` | `{name, basedOnTypeName?, thickness?, materialId?, materialName?}` | `{id, name, created, basedOn, thickness, materialId, materialName, layers}` |
|
|
217
|
+
| `document/new` | `{templatePath, savePath, overwrite?}` | `{path, title, templatePath}` |
|
|
218
|
+
| `document/open` | `{path}` | `{path, title}` — works with no active document |
|
|
219
|
+
| `document/save` | `{}` | `{path, saved}` |
|
|
220
|
+
| `document/save-as` | `{savePath, overwrite?}` | `{path}` |
|
|
221
|
+
| `document/close` | `{save?}` | `{closed, path?, title?, activePath?, activeTitle?, activeIsScratch?}` — `{closed: false}` when nothing is open |
|
|
222
|
+
| `diagnostics` | `{}` | `{autoDismiss, capacity, dropped, dialogs: [{at, dialogId, message, result, answered}], failures: [{at, transaction, severity, message, action}]}` |
|
|
223
|
+
| `diagnostics/config` | `{autoDismiss?, clear?}` | `{autoDismiss, cleared}` |
|
|
224
|
+
| `reload` | `{}` | `{reloaded, version, loadedAt, loadedFrom, collectible, unloadedPrevious, warning?}` |
|
|
225
|
+
|
|
226
|
+
The **compact** row shape, shared by `query` / `elements` / `selection`:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{ "id": 123456, "name": "Basic Wall", "category": "Walls", "typeName": "Generic - 200mm", "level": "Level 1" }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Notes that matter:
|
|
233
|
+
|
|
234
|
+
- **Element ids are JSON integers** taken from `ElementId.Value` (Int64), never the deprecated
|
|
235
|
+
`ElementId.IntegerValue`.
|
|
236
|
+
- **`query` caps `limit` at 500** (default 100). A larger value is clamped, not rejected.
|
|
237
|
+
- **`elements` returns only the parameters you name** in `params`. It never dumps every parameter
|
|
238
|
+
an element has. Omit `params` and you get identity fields only. A named parameter that does not
|
|
239
|
+
exist comes back as `null`; a missing element id comes back as `{"id": N, "found": false}` rather
|
|
240
|
+
than failing the whole batch.
|
|
241
|
+
- **`parameters/set` writes instance parameters only.** Writing to the type would silently change
|
|
242
|
+
every other instance of that type, which is never what a caller naming specific ids meant.
|
|
243
|
+
- **`elements/delete` can report more deleted than requested** — Revit also removes dependent
|
|
244
|
+
elements.
|
|
245
|
+
- **`sheets/create` skips instead of failing.** A `number` Revit refuses — practically always one
|
|
246
|
+
that already exists — lands in `skipped` with Revit's own reason, and the rest of the batch still
|
|
247
|
+
gets created, so re-running the same call after a partial run is safe. `titleBlockId` is optional
|
|
248
|
+
and defaults to the first loaded title block; a document with **none** loaded is a `NO_TITLEBLOCK`
|
|
249
|
+
failure rather than a pile of sheets with no title block. An inactive `FamilySymbol` is activated
|
|
250
|
+
first, because `ViewSheet.Create` throws on one that has never been used.
|
|
251
|
+
- **`toposolid/create` prefers Toposolid and says when it did not.** `Autodesk.Revit.DB.Toposolid`
|
|
252
|
+
is Revit 2024+ and is present in the 2025 reference assemblies this add-in compiles against, so
|
|
253
|
+
the Toposolid path is the one that is taken whenever the document has a `ToposolidType` — which
|
|
254
|
+
every 2024+ template does. The legacy `TopographySurface` is used **only** when the document has
|
|
255
|
+
no toposolid type at all, and the response says which element kind you got in `type`. `level` is
|
|
256
|
+
optional because `Toposolid.Create` needs one and the caller usually does not care: omitted, it is
|
|
257
|
+
the lowest level in the document. Point `z` is the surface elevation and is never flattened.
|
|
258
|
+
- **`floors/create` closes the boundary for you** when the last point is not the first, and takes
|
|
259
|
+
every point at the level's elevation so the floor lands on the level rather than at an accidental
|
|
260
|
+
offset. `area` is `HOST_AREA_COMPUTED` read after a `Regenerate()`, so it is the real computed
|
|
261
|
+
area and not zero. `offset` is the floor's height offset from that level in feet, written to
|
|
262
|
+
`FLOOR_HEIGHTABOVELEVEL_PARAM` - the enum name behind Revit's "Height Offset From Level"; there is
|
|
263
|
+
no `FLOOR_HEIGHTOFFSET_PARAM` in the API - and defaulting to 0, which is exactly what the endpoint
|
|
264
|
+
did before it took one. The response reports the value **read back off the parameter**, not the
|
|
265
|
+
one that was asked for.
|
|
266
|
+
- **`toposolid/flatten` is the other half of putting paving on graded terrain.** A floor laid over a
|
|
267
|
+
sloped toposolid gives Revit's "Highlighted toposolid and floor overlap" warning, and there are
|
|
268
|
+
only two honest answers: level the surface under the paving, or lift the paving off it with
|
|
269
|
+
`floors/create`'s `offset`. This endpoint does the first. It adds the ring to the toposolid's
|
|
270
|
+
shape at `elevation` (`SlabShapeEditor.AddPoints`), creases the ring
|
|
271
|
+
(`SlabShapeEditor.AddSplitLine` - `DrawSplitLine` is deprecated in 2025) so the flat region ends
|
|
272
|
+
at its boundary instead of sloping on into the terrain, and moves every existing shape vertex
|
|
273
|
+
inside the ring to the same elevation. Only a Revit 2024+ `Toposolid` can be flattened; a legacy
|
|
274
|
+
`TopographySurface` answers `NO_TOPOSOLID`. `toposolidId` is only needed when the document has
|
|
275
|
+
more than one.
|
|
276
|
+
|
|
277
|
+
`SlabShapeEditor.ModifySubElement` takes "the new value of the vertex offset" and the API
|
|
278
|
+
documents **no datum for it**. Rather than guess, the endpoint measures: one vertex is offset by
|
|
279
|
+
1 ft, then by 2 ft, and where it lands answers the question outright - a relative offset stacks
|
|
280
|
+
(the second lands 2 ft above the first), an absolute one does not (it lands 1 ft above), and the
|
|
281
|
+
datum falls out of the first measurement. `offsetMode` says which it found. `residual` is the
|
|
282
|
+
largest distance any vertex in the region is **still** off the target, measured after the fact: a
|
|
283
|
+
caller asserts that rather than trusting any of this. A crease Revit refuses is counted in
|
|
284
|
+
`creases`, not thrown - the region is still flattened, it just meets the terrain across triangles
|
|
285
|
+
instead of along an edge.
|
|
286
|
+
|
|
287
|
+
It is worth saying plainly: this is the one endpoint here whose Revit-side behaviour is not
|
|
288
|
+
pinned down by the API documentation, which is exactly why it measures itself. **Read `residual`
|
|
289
|
+
before believing the region is flat.** If it comes back large, the working fallback is the one
|
|
290
|
+
that needs no sub-element editing at all: author the terrain flat under the paved areas
|
|
291
|
+
(`toposolid/create` with the paved region's points already at the paving level) and use
|
|
292
|
+
`floors/create`'s `offset` for the rest.
|
|
293
|
+
- **`families/load` is what makes any of the family endpoints useful.** Autodesk's library is an
|
|
294
|
+
optional download that lives outside the project, under
|
|
295
|
+
`C:\ProgramData\Autodesk\RVT <year>\Libraries\<language>\`; until an `.rfa` has been loaded
|
|
296
|
+
into *this* document it does not exist to `families/symbols` or `families/place`.
|
|
297
|
+
|
|
298
|
+
It calls **`Document.LoadFamily(string, IFamilyLoadOptions, out Family)`** — the three-argument
|
|
299
|
+
overload, never the bare `LoadFamily(path)`. That is the whole reason a family already in the
|
|
300
|
+
project does not throw: without an `IFamilyLoadOptions` there is nowhere for Revit to ask its
|
|
301
|
+
"this family already exists" question and it raises instead. What the bridge answers:
|
|
302
|
+
|
|
303
|
+
| callback | answer | why |
|
|
304
|
+
| --- | --- | --- |
|
|
305
|
+
| `OnFamilyFound` | `true`, `overwriteParameterValues = false` | Reload the definition — that is what the caller asked for — but keep the parameter values **this project** has set on the types. A reload that silently reverts a tuned type to library defaults is worse than one that leaves it. Same as Revit's own "Overwrite the existing version" button. |
|
|
306
|
+
| `OnSharedFamilyFound` | `true`, `source = FamilySource.Project`, `overwriteParameterValues = false` | A nested shared family the project already has stays as the project has it; taking the incoming file's copy would change instances that are already placed and that nobody in the request mentioned. |
|
|
307
|
+
|
|
308
|
+
**A library one release behind loads.** Measured, not assumed: `RVT 2026` families
|
|
309
|
+
(`M_RPC Tree - Deciduous`, `M_RPC Shrub`, `M_Park Bench`, `M_Bollard Light`) loaded into Revit
|
|
310
|
+
2027 upgrade on the way in and produce **no** dialog and **no** transaction warning —
|
|
311
|
+
`warnings` came back `[]` and the bridge log recorded nothing. `warnings` carries whatever
|
|
312
|
+
`BridgeDiagnostics` did record during the call, so an upgrade that *does* warn is reported rather
|
|
313
|
+
than left for a caller who would have to know to go and look at `diagnostics`.
|
|
314
|
+
|
|
315
|
+
**Read `loaded` and `alreadyLoaded` together.** `alreadyLoaded` is whether the project had the
|
|
316
|
+
family before the call; `loaded` is whether Revit actually wrote it in. `loaded: false` with
|
|
317
|
+
`alreadyLoaded: true` is **not** a failure — Revit found its copy identical to the file and did
|
|
318
|
+
nothing — and that row's `familyName` and `symbols` are still the ones to place with. One
|
|
319
|
+
`TransactionGroup` for the batch, one transaction per file, and a per-file catch: a missing path
|
|
320
|
+
is `FILE_NOT_FOUND` on its own row, an `.rfa` Revit refuses is `LOAD_FAILED` on its own row, and
|
|
321
|
+
every other file in the request still loads.
|
|
322
|
+
- **`families/place` catches per point, and does something with `z` you have to know about.** The
|
|
323
|
+
symbol is activated first if it has never been used (its own inner transaction, like
|
|
324
|
+
`sheets/create`), then one inner transaction per point inside the one group: a point Revit refuses
|
|
325
|
+
lands in `failed` and the other 199 still assimilate into a single undo entry. `rotation` is
|
|
326
|
+
**radians** about the vertical axis — radians is Revit's internal angle unit exactly as feet is
|
|
327
|
+
its internal length unit.
|
|
328
|
+
|
|
329
|
+
**Which overload, and the elevation trap in it.** Point-based placement uses
|
|
330
|
+
`Document.Create.NewFamilyInstance(XYZ, FamilySymbol, Level, StructuralType)`. Planting and site
|
|
331
|
+
families are `OneLevelBased`, **not** `OneLevelBasedHosted` — a tree is not hosted by the terrain
|
|
332
|
+
it stands on, it sits at an elevation — so the overloads taking an `Element host` or a `Face` are
|
|
333
|
+
the wrong ones for them.
|
|
334
|
+
|
|
335
|
+
That overload reads `location.Z` as **the offset from the level, not as a model elevation.**
|
|
336
|
+
Measured against Revit 2027 with a real `M_RPC Tree - Deciduous`:
|
|
337
|
+
|
|
338
|
+
| asked | level elevation | `LocationPoint.Z` Revit gave |
|
|
339
|
+
| --- | --- | --- |
|
|
340
|
+
| `z: 0` on `Cota do jardim` | `-1.476378` | `-1.476378` |
|
|
341
|
+
| `z: 0` on `Piso 1` | `9.84252` | `9.84252` |
|
|
342
|
+
| `z: 5` on `Cota do jardim` | `-1.476378` | `3.523622` |
|
|
343
|
+
|
|
344
|
+
So a caller passing the absolute elevations every other endpoint here takes would have had every
|
|
345
|
+
instance pushed up or down by the level elevation, in silence. **This endpoint therefore takes
|
|
346
|
+
`z` as an absolute model elevation like the rest of the bridge and subtracts the level elevation
|
|
347
|
+
before handing the point to Revit**, and reports `placedZ` read back off each instance so the
|
|
348
|
+
result is never a matter of trust. `level` is optional and defaults to the lowest level in the
|
|
349
|
+
document; the top-level `z` is an extra offset added to every point.
|
|
350
|
+
|
|
351
|
+
One caveat on `placedZ`: it is `LocationPoint.Point.Z`, which for some families is not the base of
|
|
352
|
+
the object. `M_Bollard Light` placed at `-1.476` reports `placedZ` `1.024` while its
|
|
353
|
+
`Elevation from Level` is `0` and it stands correctly on the ground — that family's location
|
|
354
|
+
point is simply defined above its base.
|
|
355
|
+
- **`views/create-*` and `views/duplicate` never fail on a name.** View names are unique
|
|
356
|
+
document-wide; a taken one gets `" 2"`, `" 3"`, ... appended and the response says what the view
|
|
357
|
+
is actually called. Losing a batch of plans to one collision is worse than a suffix.
|
|
358
|
+
- **`views/create-section` geometry.** `origin` is the model point the section is centred on and the
|
|
359
|
+
cut plane passes through it. **`direction` is the horizontal direction the section LOOKS TOWARD** -
|
|
360
|
+
`{x: 0, y: 1}` is a section looking north. It is what the view faces, not where the viewer stands;
|
|
361
|
+
its `z` is ignored and it is normalised. `width` is the extent across the view along the section
|
|
362
|
+
line, `height` the vertical extent (up is always +Z), both centred on `origin`. `depth` is how far
|
|
363
|
+
in front of `origin` the view sees, and is where the far clip lands.
|
|
364
|
+
|
|
365
|
+
**The transform is settled by measurement against a live Revit 2027, not by reading
|
|
366
|
+
`RevitAPI.xml`.** Three sections were created with `BasisZ = -direction`, `BasisX = direction × Z`,
|
|
367
|
+
and the created views' own frames read back off the views:
|
|
368
|
+
|
|
369
|
+
| asked `direction` | `viewDirection` | `rightDirection` | `upDirection` |
|
|
370
|
+
| --- | --- | --- | --- |
|
|
371
|
+
| `{x: 0, y: 1}` | `{0, 1, 0}` | `{-1, 0, 0}` | `{0, 0, 1}` |
|
|
372
|
+
| `{x: 0, y: -1}` | `{0, -1, 0}` | `{1, 0, 0}` | `{0, 0, 1}` |
|
|
373
|
+
| `{x: 1, y: 0}` | `{1, 0, 0}` | `{0, 1, 0}` | `{0, 0, 1}` |
|
|
374
|
+
|
|
375
|
+
So Revit hands back `viewDirection = -BasisZ` and `rightDirection = -BasisX`: it turns the frame
|
|
376
|
+
it is given by 180° about up. `View.ViewDirection` is "the direction towards the viewer", so a
|
|
377
|
+
`viewDirection` **equal** to the asked direction means those sections looked the opposite way -
|
|
378
|
+
backwards from what this endpoint promises. The right directions say it independently: asked
|
|
379
|
+
`{0, 1}`, Revit reported right `{-1, 0, 0}`, west, which is the right hand of a viewer facing
|
|
380
|
+
**south**. That was a real bug, and it is fixed by negating the direction that goes into the
|
|
381
|
+
transform - the parameter still means what it always said it meant.
|
|
382
|
+
|
|
383
|
+
Measured rule, and the one this endpoint is built on: **the view looks along the box's `BasisZ`
|
|
384
|
+
and reports `viewDirection = -BasisZ`.** To look toward `direction`, hand it `BasisZ = direction`.
|
|
385
|
+
`BoundingBoxXYZ.Transform` remarks that "the transform must always be right-handed and
|
|
386
|
+
orthonormal", which then fixes `BasisX = BasisY × BasisZ = Z × direction`; Revit reports
|
|
387
|
+
`rightDirection = -BasisX = direction × Z`, east for a section looking north, which is what is on
|
|
388
|
+
the right of a viewer facing north. `Min`/`Max` are `(-width/2, -height/2, -depth)` /
|
|
389
|
+
`(width/2, height/2, 0)` in that frame, so the far clip distance is `depth` and the near plane is
|
|
390
|
+
the cut plane through `origin`; being in the box frame, the depth region turns with `BasisZ` and
|
|
391
|
+
stays in front of the way the view looks.
|
|
392
|
+
|
|
393
|
+
Do not re-derive any of this from `RevitAPI.xml`. `ViewSection.CreateSection` remarks that "the
|
|
394
|
+
view direction of the resulting section will be `sectionBox.Transform.BasisZ`", but its "view
|
|
395
|
+
direction" is the way the view **looks**, the opposite of the `View.ViewDirection` property of the
|
|
396
|
+
same name; its other remark, that `(right, up, view direction)` is "left handed", reads either way
|
|
397
|
+
depending on which of the two is meant. Reading those remarks as `View.ViewDirection` is exactly
|
|
398
|
+
what put the sign the wrong way round in the first place. The measurements are the authority.
|
|
399
|
+
|
|
400
|
+
**The created view's own frame is reported precisely so this is assertable.** `views/create-section`
|
|
401
|
+
answers with the view's `viewDirection`, `rightDirection` and `upDirection` as `{x, y, z}`, and so
|
|
402
|
+
does every framed row of `views`: **a caller asserts what Revit did** instead of opening Revit and
|
|
403
|
+
looking at it. For a section looking toward **D**, Revit reports `viewDirection` **-D** - a section
|
|
404
|
+
asked to look at `{x: 0, y: 1}` reports `viewDirection` `{x: 0, y: -1, z: 0}`, and that is the
|
|
405
|
+
assertion, not a bug. A view with no frame at all - a schedule, a legend - simply does not
|
|
406
|
+
carry the three fields.
|
|
407
|
+
- **`views/create-3d` aims the camera for you, and tells you where the model is.**
|
|
408
|
+
`ViewOrientation3D` takes `(eyePosition, upDirection, forwardDirection)` and Revit requires `up`
|
|
409
|
+
perpendicular to `forward`, so neither can be passed through raw. From an `eye` and a `target` the
|
|
410
|
+
bridge builds:
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
forward = normalize(target - eye) the direction the camera looks
|
|
414
|
+
right = normalize(forward x Z) horizontal; +X when the camera looks north
|
|
415
|
+
up = right x forward world up, tilted with the camera
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
A camera looking straight down or straight up has no horizontal right vector (`forward x Z` is
|
|
419
|
+
zero), and that one case falls back to `right = +X`, which puts north at the top of the image the
|
|
420
|
+
way a plan does. Verified live: `eye {93, 6, 7} -> target {52, 60, 8}` came back with
|
|
421
|
+
`viewDirection {-0.055, -0.997, -0.055}` (Revit's view direction is `-forward`),
|
|
422
|
+
`rightDirection {0.998, -0.055, 0}` and `upDirection {-0.003, -0.055, 0.998}`; straight down gave
|
|
423
|
+
`viewDirection {0, 0, 1}`, `up {0, 1, 0}`, `right {1, 0, 0}`.
|
|
424
|
+
|
|
425
|
+
Every response also carries **`modelExtents`** - `{min, max, center}` over everything modelled -
|
|
426
|
+
because a camera cannot be aimed without knowing where the model is and no other endpoint says.
|
|
427
|
+
Create a view with no `eye`/`target`, read the extents, create the one you want. Model categories
|
|
428
|
+
only, and two exclusions that were **measured rather than assumed**: a `ViewSheet` reports a Model
|
|
429
|
+
category with a bounding box at `z = -1000`, and a camera (the glyph standing for a 3D view) sits
|
|
430
|
+
above everything built. Levels and grids drop out with the rest of the annotation categories. On
|
|
431
|
+
`moradia.rvt` that is the difference between `{-328, -328, -1000} .. {328, 328, 87}` and the real
|
|
432
|
+
`{-1.39, -2.22, -4.6} .. {100.75, 83.95, 29}`.
|
|
433
|
+
|
|
434
|
+
A perspective view has no view scale - Revit reports `0` and refuses the setter - so `scale` is
|
|
435
|
+
applied only to an isometric.
|
|
436
|
+
- **`views/set-style` sets the display style and the detail level; shadows moved out.**
|
|
437
|
+
`View.DisplayStyle` and `View.DetailLevel` are plain settable properties and both are read back
|
|
438
|
+
off the view after the write, because a view template owns them on the views that use one.
|
|
439
|
+
`"HiddenLine"` is accepted for `DisplayStyle.HLR`, which is named after hidden line removal and is
|
|
440
|
+
not what anybody outside the API calls it.
|
|
441
|
+
|
|
442
|
+
`shadows` is answered `409 SHADOWS_HANDLED_ELSEWHERE` pointing at `views/set-graphics`. This
|
|
443
|
+
endpoint used to answer `SHADOWS_NOT_EXPOSED` and say cast shadows were impossible; that claim was
|
|
444
|
+
too broad and has been withdrawn. What remains true is that `View` has no
|
|
445
|
+
`EnableSunlightAndShadows`, `View.ShadowIntensity` / `View.SunlightIntensity` are the Lighting
|
|
446
|
+
sliders rather than the switch, and `View.SunAndShadowSettings` is read-only and holds the sun
|
|
447
|
+
*position*. What was wrong is the rest: `BuiltInParameter.GRAPHIC_DISPLAY_OPTIONS_SHADOWS` exists,
|
|
448
|
+
and whether it is a usable toggle is a property of the live view. See `GraphicsEndpoints`.
|
|
449
|
+
- **`views/graphics` and `views/set-graphics` probe cast shadows rather than declaring them.**
|
|
450
|
+
The enum member existing proves nothing - Autodesk's own REVIT-222419 is the record of an enum
|
|
451
|
+
member that is not a usable toggle - so `get_Parameter(GRAPHIC_DISPLAY_OPTIONS_SHADOWS)` is asked
|
|
452
|
+
on the live view and everything it answers is reported: `available`, `storageType`, `readOnly`,
|
|
453
|
+
`value`, `on`, whether a view template controls it, and a `writable` verdict with the reason
|
|
454
|
+
behind it. `on: null` means UNKNOWN, never off.
|
|
455
|
+
|
|
456
|
+
`views/set-graphics` then takes one of two routes and says which:
|
|
457
|
+
|
|
458
|
+
- **`method: "parameter"`** when the probe proves all four conditions - present, `Integer`
|
|
459
|
+
storage, not read-only, not template-controlled. It is written inside the same transaction group
|
|
460
|
+
as `style`/`detailLevel`/the intensities and read back afterwards, so this is the only route that
|
|
461
|
+
can report `verified: true`.
|
|
462
|
+
- **`method: "posted-command"`** otherwise. `RevitCommandId.LookupCommandId("ID_IMAGE_SHADOW_ON"
|
|
463
|
+
/ "ID_IMAGE_SHADOW_OFF")` - both strings are present in Revit 2027's `UIFrameworkRes.dll`,
|
|
464
|
+
`DesktopMFC.dll` and `Utility.dll` - is gated on `CanPostCommand` and posted with
|
|
465
|
+
`UIApplication.PostCommand`. There is no `PostableCommand` member for shadows, so a `false` from
|
|
466
|
+
`CanPostCommand` is reported as `method: "unavailable"` rather than worked around. A posted
|
|
467
|
+
command acts on the **active** view, so the target view is made active first with
|
|
468
|
+
`UIDocument.ActiveView` - which Revit only allows while the document is not modifiable, hence
|
|
469
|
+
outside the transaction group, after it has closed. It runs when control returns to Revit, so
|
|
470
|
+
the response is `posted: true, pending: true, unverified: true, verified: false` and never says
|
|
471
|
+
success. Revit allows one posted command at a time, so a second while one is outstanding is
|
|
472
|
+
`method: "blocked"`, and a request that needs this route must name a single view.
|
|
473
|
+
|
|
474
|
+
`views/graphics-command-status` reports what became of it, including a fresh live probe. When the
|
|
475
|
+
parameter cannot be read back, `verified` stays `false` with `verifiedBy: null`.
|
|
476
|
+
|
|
477
|
+
The Idling handler that timestamps the first idle after a post is **one-shot and unsubscribes
|
|
478
|
+
itself first thing**. That is not tidiness: these handlers live in a collectible
|
|
479
|
+
`AssemblyLoadContext` and a delegate left on Revit's `Idling` event would pin the context and
|
|
480
|
+
break `/revit-mcp/reload`.
|
|
481
|
+
|
|
482
|
+
`ambientLightIntensity` is refused `409 AMBIENT_LIGHT_NOT_EXPOSED` rather than accepted and
|
|
483
|
+
ignored: checked against both the 2025 reference assembly and the installed 2027 `RevitAPI.dll`,
|
|
484
|
+
`View` has `ShadowIntensity` and `SunlightIntensity` and no ambient equivalent, and there is no
|
|
485
|
+
ambient `BuiltInParameter` on a view either.
|
|
486
|
+
- **`views/capture-template` states what a template controls; `views/apply-template` validates the
|
|
487
|
+
whole batch first.** `View.CreateViewTemplate()` copies the source view and
|
|
488
|
+
`SetNonControlledTemplateParameterIds` is what turns the copy into a template with a deliberate
|
|
489
|
+
scope. `mode: "graphics"` controls everything the template can except `VIEW_GRAPH_SUN*` /
|
|
490
|
+
`VIEW_SOLARSTUDY*` (so each view keeps its own sun), `VIEWER_*` (crop, camera, extents) and
|
|
491
|
+
`VIEW_PHASE` / `VIEW_PHASE_FILTER`; every exclusion comes back with its reason and the controlled
|
|
492
|
+
set is read back off the template rather than echoed.
|
|
493
|
+
|
|
494
|
+
`views/apply-template` checks each target is a view, is not a template or a sheet, and passes
|
|
495
|
+
`View.IsValidViewTemplate` **before** writing anything - one failure fails the request with all
|
|
496
|
+
the reasons rather than leaving half the batch changed. `mode: "apply"` is
|
|
497
|
+
`ApplyViewTemplateParameters`, a one-time copy; `mode: "assign"` sets `ViewTemplateId`, a lasting
|
|
498
|
+
association, and will not detach a template a view already has without `replace: true`. `dryRun`
|
|
499
|
+
defaults to **true**.
|
|
500
|
+
|
|
501
|
+
Neither endpoint pretends a template can conjure state the source did not have: capturing from a
|
|
502
|
+
view whose cast shadows are off produces a template that turns shadows on nowhere, and both
|
|
503
|
+
responses carry the probes that make that visible.
|
|
504
|
+
- **`views/export-image` reports the file Revit wrote, not the one you asked for.** `ExportImage`
|
|
505
|
+
treats `FilePath` as a **stem** and appends ` - <view type> - <view name>` to it: asking for
|
|
506
|
+
`renders\moradia.png` on a view called `MCP Garden Eye` writes
|
|
507
|
+
`renders\moradia - 3D View - MCP Garden Eye.png`. So the directory is listed before and after the
|
|
508
|
+
export and the file that appeared - or, on a re-export, the file whose timestamp moved - is what
|
|
509
|
+
comes back in `path`; `requestedPath` is only there to make the difference visible. A batch is run
|
|
510
|
+
as **one single-view export per view** for exactly this reason: with one view in flight there is
|
|
511
|
+
exactly one file to attribute.
|
|
512
|
+
|
|
513
|
+
`ImageExportOptions.GetFileName` is deliberately **not** used as the stem. It returns the suffix
|
|
514
|
+
Revit is about to append (`" - 3D View - <name>"`, with an empty base), so using it writes the
|
|
515
|
+
suffix twice - `renders\ - 3D View - Iso - 3D View - Iso.png`, which is what it actually did
|
|
516
|
+
before this was fixed. The default stem is the document title, the way Revit's own export dialog
|
|
517
|
+
names a file, and the default folder is `<user profile>\RevitProjects
|
|
518
|
+
enders\`.
|
|
519
|
+
|
|
520
|
+
Size is Revit's model, not a width and a height: one `PixelSize` along one `FitDirection`, with
|
|
521
|
+
the other dimension following the view. `width` fits horizontally (1600 px by default), `height`
|
|
522
|
+
fits vertically, passing both fits the width - and the `width`/`height` in the response are read
|
|
523
|
+
out of the written PNG's IHDR chunk rather than echoed. `HLRandWFViewsFileType` and
|
|
524
|
+
`ShadowViewsFileType` are both set: Revit picks whichever matches how the view is drawn, and a
|
|
525
|
+
shaded view exported with only the first set comes out in the wrong format. Nothing is transacted
|
|
526
|
+
- exporting is a read, and Revit refuses it inside a transaction.
|
|
527
|
+
|
|
528
|
+
**This is not a render.** The Revit API cannot start the photoreal raytracer:
|
|
529
|
+
`View3D.GetRenderingSettings` / `SetRenderingSettings` configure what the Render dialog *would*
|
|
530
|
+
do, `Document` has `ExportImage` and `SaveToProjectAsImage` and no render method at all, and
|
|
531
|
+
`IPhotoRenderContext` is the hook for a *third-party* renderer to receive geometry through
|
|
532
|
+
`CustomExporter`, not a way to run Revit's own. What comes out is the view exactly as drawn on
|
|
533
|
+
screen, which is why `views/set-style` is the setting that matters - a `Realistic` export shows
|
|
534
|
+
materials and RPC content properly, a `HiddenLine` one does not. Same view on `moradia.rvt`:
|
|
535
|
+
44 KB hidden-line, 457 KB realistic.
|
|
536
|
+
- **`views/create-drafting` is a view of nothing**, which is the point: a drafting view carries
|
|
537
|
+
linework and annotation and no model geometry at all. It is what a detail sheet is made of - fill
|
|
538
|
+
it with `detail/lines` and `detail/text`, then put it on a sheet like any other view.
|
|
539
|
+
- **`views/create-plan` takes a view family type by NAME.** `viewFamilyType` is the name Revit shows
|
|
540
|
+
for a `ViewFamilyType` in this document - `"Site"`, `"Ceiling Plan"`, `"Structural Plan"` - not a
|
|
541
|
+
`ViewFamily` enum name, because a Site plan is an ordinary FloorPlan-family type and only its name
|
|
542
|
+
tells it apart from `"Floor Plan"`. Matching on the family would make a Site plan unreachable.
|
|
543
|
+
Omitted, it is the first FloorPlan type, exactly as before. Only the four families `ViewPlan.Create`
|
|
544
|
+
documents are accepted - "the type needs to be a FloorPlan, CeilingPlan, AreaPlan, or StructuralPlan
|
|
545
|
+
ViewType" - and anything else is a `BAD_REQUEST` carrying the plan type names the document does
|
|
546
|
+
have, rather than Revit's "This view family type is not a plan view type".
|
|
547
|
+
- **Scale is read back off the view, never echoed.** `views/create-plan`, `views/create-drafting`,
|
|
548
|
+
`views/create-section` and `views/set-scale` all report `scale` as `View.Scale` after the write, so
|
|
549
|
+
a view template that owns the scale shows up as a difference between what was asked for and what
|
|
550
|
+
came back instead of being invisible. `views/set-scale` validates once with `View.IsValidViewScale`,
|
|
551
|
+
which documents the range as 1 to 24,000, then works per view: a schedule, a sheet or a template
|
|
552
|
+
lands in `failed` with `VIEW_SCALE_NOT_SETTABLE` while the rest of the batch is still re-scaled and
|
|
553
|
+
still assimilates into one undo entry. Those three are refused up front on purpose - a schedule and
|
|
554
|
+
a sheet have no view scale, and a view template's scale is inherited by every view using it, so
|
|
555
|
+
re-scaling one from a batch would silently re-scale views nobody named. Anything else is attempted
|
|
556
|
+
and Revit's own refusal comes back per view as `REVIT_API_ERROR`.
|
|
557
|
+
- **A perspective view has no view scale, and that is a different endpoint, not a different number.**
|
|
558
|
+
`views/set-scale` refuses one with its own code, `PERSPECTIVE_VIEW_HAS_NO_SCALE`, rather than
|
|
559
|
+
letting Revit's exception on the setter come back as a bare `REVIT_API_ERROR`: 1/X describes a
|
|
560
|
+
projection and a camera is not one. What re-sizes a perspective on its sheet is
|
|
561
|
+
`views/scale-perspective-crop`, which wraps `View3D.ScalePerspectiveCropBox(double)` - Revit's own
|
|
562
|
+
method, present since 2024.1, which scales the crop box on both axes and, in its documentation's
|
|
563
|
+
words, "makes the change analogous to changing the scale of the orthographic view, so that both the
|
|
564
|
+
size and scale of the view on a sheet changes". So `multiplier` 2 doubles the view on the paper and
|
|
565
|
+
0.5 halves it, with the proportions locked and the framing identical. It is **not** a reframe:
|
|
566
|
+
`views/set-crop` changes what is in shot, this changes the size of the shot, and the camera is never
|
|
567
|
+
touched - which is why the response carries `camera` on both sides and `cameraUnchanged` comparing
|
|
568
|
+
them, with `null` there meaning an orientation could not be read rather than that it moved. The four
|
|
569
|
+
checks - element exists, is a `View3D`, is not a template (Revit's method throws on one), is
|
|
570
|
+
perspective - all run before the transaction opens, so a refusal never half-applies. `dryRun`
|
|
571
|
+
defaults to **true** and reports `before` plus the requested `multiplier` and deliberately no
|
|
572
|
+
predicted `after`: the size Revit lands on is a readback, and the document is regenerated inside the
|
|
573
|
+
transaction before it is taken, because `View.Outline` and the viewport's box on the sheet are
|
|
574
|
+
derived geometry that otherwise still read as the size from before the call. Verified live on view
|
|
575
|
+
223065 at `multiplier` 5.64896: `outline` 0.492 x 0.369 -> 2.78 x 2.085 paper feet, the viewport
|
|
576
|
+
0.512 x 0.389 -> 2.8 x 2.105, `cameraUnchanged` true. Two measured nuances came out of that run and
|
|
577
|
+
are worth stating, because both look like faults and neither is: the crop box's **model**
|
|
578
|
+
coordinates did not change at all - composition is kept along with the camera, so `outline` and
|
|
579
|
+
`viewport` are what say the call worked and `cropBox` is not - and Revit left the view **title** at
|
|
580
|
+
its old paper position, so the label offset ended up inside the enlarged image and had to be reset
|
|
581
|
+
through `sheets/set-viewport-position`. That reset is deliberately a separate call: where a view
|
|
582
|
+
title sits is a drawing decision, and an endpoint that re-sizes a view does not get to make it.
|
|
583
|
+
- **`views/create-legend` duplicates, because the Revit API cannot create a legend.** This is a
|
|
584
|
+
limitation, not a design choice, and it is worth stating exactly: the API exposes **no legend view
|
|
585
|
+
type at all** (there is no public `ViewLegend`, and no `LegendComponent`); `ViewPlan.Create` takes
|
|
586
|
+
only the four plan families quoted above; and `ViewDrafting.Create` documents
|
|
587
|
+
`ArgumentException` - "viewFamilyTypeId is not a valid ViewFamilyType for a drafting view" - so the
|
|
588
|
+
Legend `ViewFamilyType` every template carries has nothing that will accept it. The documented way
|
|
589
|
+
through is `View.Duplicate`, which is what this endpoint does: `fromLegendId` picks the source,
|
|
590
|
+
omitted it is the first legend `views/legends` would list, and the copy is made with
|
|
591
|
+
`ViewDuplicateOption.Duplicate` so the new legend comes through **empty** rather than carrying
|
|
592
|
+
someone else's key. A document with no legend at all answers `NO_LEGEND_TO_DUPLICATE` and says so
|
|
593
|
+
in the message; it does **not** quietly substitute a drafting view, because a drafting view cannot
|
|
594
|
+
be placed on many sheets and a legend can. What can then be put in it is `detail/lines` and
|
|
595
|
+
`detail/text`. Legend components cannot - there is no creation API for them - and the bridge does
|
|
596
|
+
not pretend there is.
|
|
597
|
+
- **`parameters/create-project` borrows the shared parameter file and gives it back.** A project
|
|
598
|
+
parameter in Revit is a shared parameter definition plus a binding, and the definition lives in a
|
|
599
|
+
text file named by `Application.SharedParametersFilename` - a user-wide Revit setting, not a
|
|
600
|
+
property of the model. When it is unset (or names a file that is gone) the bridge points Revit at
|
|
601
|
+
its own file in the temp folder, `revit-mcp-shared-parameters.txt`, for the length of the call and
|
|
602
|
+
restores the original value in a `finally`. The file is stable across calls on purpose: the same
|
|
603
|
+
file means the same GUID for a parameter of the same name, so re-running a call cannot mint a
|
|
604
|
+
second definition that Revit would treat as a different parameter. `type` defaults to `Text`
|
|
605
|
+
(`SpecTypeId.String.Text`), `group` to `IdentityData`, and `instance` to true - a `TypeBinding`
|
|
606
|
+
otherwise. A name already bound in the document is **not** an error: nothing is created,
|
|
607
|
+
`alreadyExisted` is true, and `categories` reports what it is really bound to.
|
|
608
|
+
- **`sheets/set-parameter` is not `parameters/set` with a different name.** `parameters/set` writes
|
|
609
|
+
ONE name and ONE value across a list of ids, all-or-nothing. This one writes a **different** value
|
|
610
|
+
per sheet - which is what stamping a phase across 56 sheets is - in one `TransactionGroup` with one
|
|
611
|
+
inner transaction per sheet, so a sheet that has no such parameter lands in `failed` with
|
|
612
|
+
`PARAMETER_NOT_FOUND` while the other 55 are written. Both write instance parameters only; they
|
|
613
|
+
share the same storage-type switch (`WriteEndpoints.ApplyValue`), so a value is coerced identically
|
|
614
|
+
either way.
|
|
615
|
+
- **Sheet grouping in the Project Browser is not settable from the API, so there is no endpoint for
|
|
616
|
+
it.** `BrowserOrganization` is read-only in both the 2025 assemblies this add-in compiles against
|
|
617
|
+
and the installed 2027 ones. The whole public surface is
|
|
618
|
+
`GetCurrentBrowserOrganizationFor{Sheets,Views,Schedules}(document)`, `GetFolderItems(elementId)`,
|
|
619
|
+
`AreFiltersSatisfied(elementId)`, and three **get-only** properties -`SortingParameterId`,
|
|
620
|
+
`SortingOrder` and `Type`. There is no setter, no `Create`, no way to add a folder rule and no way
|
|
621
|
+
to apply one; `FolderItemInfo` is equally a read-only report. Shipping a `browser/organize-sheets`
|
|
622
|
+
that quietly did nothing would be worse than not having one, so the bridge has none. Two things do
|
|
623
|
+
work: sheets sort by sheet number, so a numbering scheme like `L.02.xxx` / `L.03.xxx` already
|
|
624
|
+
groups a set in reading order without any parameter at all; and the folder organisation is a
|
|
625
|
+
one-off UI setting - right-click **Sheets** in the Project Browser > **Browser Organization** -
|
|
626
|
+
pointed at the parameter `parameters/create-project` created and `sheets/set-parameter` filled in.
|
|
627
|
+
It is saved in the model once it is set.
|
|
628
|
+
- **`detail/lines` and `detail/text` supply the view plane themselves.** `NewDetailCurve` refuses a
|
|
629
|
+
curve that is not in the plane of the view, and `View.Origin` is documented as "not meaningful"
|
|
630
|
+
for a plan, so the elevation comes from the plan's own level (a drafting view is simply 0) and the
|
|
631
|
+
response reports it in `elevation`. `x`/`y` are the view's plan coordinates in feet.
|
|
632
|
+
`detail/lines` refuses anything that is not a drafting view, a plan or a legend with
|
|
633
|
+
`VIEW_CANNOT_HOST_DETAIL`; `detail/text` is wider - a section or an elevation may be annotated -
|
|
634
|
+
and only refuses schedules, sheets and templates, with `VIEW_CANNOT_HOST_TEXT`. An unknown
|
|
635
|
+
`lineStyle` is **not** a failure: the lines are drawn in the view's default style and the response
|
|
636
|
+
carries `requestedLineStyle` plus `availableLineStyles`, the same shape `schedules/create` uses
|
|
637
|
+
for fields. Text size lives on the type rather than on the note, so a `size` no loaded type
|
|
638
|
+
carries means a duplicated `TextNoteType` - once per distinct size, in its own transaction,
|
|
639
|
+
reusing any existing type whose `TEXT_SIZE` already matches.
|
|
640
|
+
- **`sheets/place-view` branches on the view kind so the caller does not have to.** A `ViewSchedule`
|
|
641
|
+
goes on a sheet through `ScheduleSheetInstance.Create`, everything else through `Viewport.Create`;
|
|
642
|
+
`kind` in the response says which was used. A view already on a sheet is `VIEW_ALREADY_PLACED`
|
|
643
|
+
(Revit allows a view on exactly one sheet — duplicate it to show it twice); a view
|
|
644
|
+
`Viewport.CanAddViewToSheet` refuses is `CANNOT_PLACE`. A schedule is the exception to the first
|
|
645
|
+
rule: it may legitimately appear on several sheets, so only a repeat of the *same* sheet/schedule
|
|
646
|
+
pair is refused. The **legend** case is a known sharp edge: Revit allows one legend on many sheets
|
|
647
|
+
and the bridge does not, because there is one rule for viewports. `x`/`y` are sheet coordinates in
|
|
648
|
+
feet on the paper — an A1 sheet is 1.95 × 1.38 — and default to the middle of `ViewSheet.Outline`.
|
|
649
|
+
The array form (`placements`) catches per placement and answers `{placed, failed}`; the single
|
|
650
|
+
form throws, so the codes surface as real errors.
|
|
651
|
+
- **`schedules/create` reports unknown fields instead of failing.** Names are matched against
|
|
652
|
+
`ScheduleDefinition.GetSchedulableFields()` for that category; anything unmatched goes into
|
|
653
|
+
`skippedFields`, and the response then also carries `availableFields` — the exact names that
|
|
654
|
+
category does offer — so the caller can correct itself without a second round trip.
|
|
655
|
+
- **`directshape/create`, `planting/place`, `pipes/create` and `sprinklers/place` exist because
|
|
656
|
+
family content is optional.** They also carry `comments` and `mark` onto every element they
|
|
657
|
+
build, give every element a real `DirectShapeType`, and build its solids with the material named
|
|
658
|
+
in `materialId`. See [Geometry without families](#geometry-without-families).
|
|
659
|
+
- **`materials`, `materials/create`, `materials/assign`, `walltypes/create` and `floortypes/create`
|
|
660
|
+
are the only way anything here stops being grey.** Which route applies depends on the element and
|
|
661
|
+
the two are not interchangeable — see [Materials](#materials). `materials/set-texture` is the
|
|
662
|
+
step after that: a colour is flat paint, a bitmap is a texture.
|
|
663
|
+
- **The whole `document/*` family is deliberately not transacted.** Creating, opening, saving and
|
|
664
|
+
closing a document are not transactable model edits and Revit throws if they are attempted inside
|
|
665
|
+
a transaction, so none of them goes through `RevitWrite`. They are also where the
|
|
666
|
+
no-active-document rule is least uniform, on purpose: `document/new` and `document/open` work with
|
|
667
|
+
nothing open (that is the state Revit is in when a caller asks for a new project); `document/save`
|
|
668
|
+
and `document/save-as` require an active document; `document/close` requires nothing and answers
|
|
669
|
+
`{"closed": false}` when there is nothing to close, so a caller tidying up never has to ask first.
|
|
670
|
+
- **Neither `document/new` nor `document/save-as` overwrites unless it is told to.** An existing
|
|
671
|
+
path is a `FILE_EXISTS` failure; both take `{"overwrite": true}` to replace it, and a missing
|
|
672
|
+
parent directory is created by both. `document/new`'s overwrite is the *rebuild in place* route,
|
|
673
|
+
and it is a real teardown: the file is closed in Revit if it is open (same detour as
|
|
674
|
+
`document/close`), then it and the `<name>.0001.rvt` backups Revit wrote beside it are deleted,
|
|
675
|
+
then the project is built again at that same path. A project is meant to live at one path and be
|
|
676
|
+
rebuilt there — not versioned into a new filename per attempt, which leaves the user a folder of
|
|
677
|
+
junk. Anything still holding a file is a `FILE_LOCKED` naming that exact path; the bridge never
|
|
678
|
+
falls back to building under a different name.
|
|
679
|
+
- **`document/save` refuses a model that has never been saved** with `NOT_SAVEABLE` rather than
|
|
680
|
+
letting Revit open its Save As file browser — which, on an unattended session, is a parked main
|
|
681
|
+
thread and a `REVIT_BUSY` for every request after it. Use `document/save-as`.
|
|
682
|
+
- **`document/close` discards unsaved changes** unless `{"save": true}`, and it can close the
|
|
683
|
+
*active* document, which `Document.Close` on its own cannot: Revit throws "The active document
|
|
684
|
+
may not be closed from the API". So the endpoint gives Revit something else to be active on
|
|
685
|
+
first — another open document, activated with `UIApplication.OpenAndActivateDocument` on its
|
|
686
|
+
`PathName` (on a file Revit already has open that is an activation, not a second open), or, when
|
|
687
|
+
this is the only document, a blank scratch project created with
|
|
688
|
+
`Application.NewProjectDocument(UnitSystem.Metric)`, saved under `%TEMP%` and activated the same
|
|
689
|
+
way. The response says what took over in `activePath` / `activeTitle` / `activeIsScratch`. The
|
|
690
|
+
scratch is left open on purpose: it costs nothing and the next close finds it as the stand-in
|
|
691
|
+
instead of making another. If the scratch cannot be stood up at all, the answer is
|
|
692
|
+
`LAST_DOCUMENT_CANNOT_CLOSE` carrying Revit's own reason — never a pretend `{"closed": true}`.
|
|
693
|
+
- **`diagnostics` is a read, `diagnostics/config` is the switch.** Neither needs an active document.
|
|
694
|
+
See [Unattended operation](#unattended-operation) for why a caller that writes should be reading
|
|
695
|
+
the first one.
|
|
696
|
+
- **`reload` is answered by the loader, not by this route table.** It replaces the assembly every
|
|
697
|
+
other endpoint lives in — see [Hot reload](#hot-reload).
|
|
698
|
+
- Any request may carry a top-level **`timeoutMs`** to override the server-side wait for that call.
|
|
699
|
+
|
|
700
|
+
### Geometry without families
|
|
701
|
+
|
|
702
|
+
`directshape/create`, `planting/place`, `pipes/create` and `sprinklers/place` build solids straight
|
|
703
|
+
into the project document, with no family and no family template behind them.
|
|
704
|
+
|
|
705
|
+
They exist because **family content is an optional Autodesk download**, and a perfectly working
|
|
706
|
+
Revit install can have none of it. When the library is genuinely absent there is no `.rfa` to load
|
|
707
|
+
and no `.rft` family template to author a replacement from either, so `families/symbols` answers
|
|
708
|
+
`[]` and `families/place` has nothing to place.
|
|
709
|
+
|
|
710
|
+
**This is the fallback, not the default.** `families/symbols` coming back empty almost always means
|
|
711
|
+
the library is installed and simply has not been loaded into *this* project yet — the library sits
|
|
712
|
+
outside the model, under `C:\ProgramData\Autodesk\RVT <year>\Libraries\<language>\`, and
|
|
713
|
+
`families/load` is what brings it in. Check there before reaching for these four: a garden built out
|
|
714
|
+
of spheres on cylinders looks exactly like a garden built out of spheres on cylinders.
|
|
715
|
+
|
|
716
|
+
`DirectShape` needs neither. `GeometryCreationUtilities` builds the solid,
|
|
717
|
+
`DirectShape.CreateElement(doc, new ElementId(BuiltInCategory.X))` hangs it off a real Revit
|
|
718
|
+
category, and the result is a proper element — visible, selectable, categorised, and schedulable by
|
|
719
|
+
`schedules/create`.
|
|
720
|
+
|
|
721
|
+
`category` is a `BuiltInCategory` name **without** the `OST_` prefix (`Planting`,
|
|
722
|
+
`LightingFixtures`, `Furniture`, `Site`, `Walls`, `GenericModel`), matched case-insensitively; the
|
|
723
|
+
prefixed form is accepted too. A name that is not a category, or one `DirectShape.IsValidCategoryId`
|
|
724
|
+
rejects, is a `BAD_REQUEST` listing examples.
|
|
725
|
+
|
|
726
|
+
Each entry in `shapes` is one element. The primitives, all in decimal feet:
|
|
727
|
+
|
|
728
|
+
| `kind` | Fields | Built with |
|
|
729
|
+
| --- | --- | --- |
|
|
730
|
+
| `cylinder` | `base: {x,y,z}`, `radius`, `height` | circle (two half arcs) extruded +Z |
|
|
731
|
+
| `box` | `min: {x,y,z}`, `max: {x,y,z}` | rectangle at `min.z` extruded +Z by `max.z - min.z` |
|
|
732
|
+
| `sphere` | `center: {x,y,z}`, `radius` | half-disc in the XZ plane revolved a full turn about +Z |
|
|
733
|
+
| `cone` | `base: {x,y,z}`, `radius`, `height` | triangle revolved a full turn about +Z |
|
|
734
|
+
| `extrusion` | `profile: [{x,y}]`, `baseZ?`, `height` | closed polygon at `baseZ` extruded +Z |
|
|
735
|
+
|
|
736
|
+
An entry may instead be `{kind: "group", name?, parts: [primitive, ...]}`, which produces **one**
|
|
737
|
+
element carrying several solids — that is how a tree is a trunk plus a crown in a single schedulable
|
|
738
|
+
element rather than two unrelated ones. Groups do not nest.
|
|
739
|
+
|
|
740
|
+
The circle is two half arcs on purpose: `CreateExtrusionGeometry` refuses a `CurveLoop` made of a
|
|
741
|
+
single closed curve.
|
|
742
|
+
|
|
743
|
+
`planting/place` is the convenience wrapper over that: one element per point in the `Planting`
|
|
744
|
+
category, each a trunk cylinder with a crown sphere sitting on top of it (the sphere's underside
|
|
745
|
+
touches the top of the trunk). Defaults in **feet** are `trunkHeight` 8, `trunkRadius` 0.5,
|
|
746
|
+
`crownRadius` 6 — a 20 ft tree.
|
|
747
|
+
|
|
748
|
+
`pipes/create` and `sprinklers/place` are the same idea for irrigation, because there is no MEP
|
|
749
|
+
family content either. A run becomes **one** element built from a cylinder per segment between
|
|
750
|
+
consecutive points — a cylinder per segment on purpose, not a real sweep along the polyline, which
|
|
751
|
+
is a pile of failure modes for a result nobody can tell apart at 1:100. `radius` is per run and
|
|
752
|
+
defaults to 0.08 ft; a sprinkler is a small cylinder, `radius` 0.15 and `height` 0.5 ft by default.
|
|
753
|
+
|
|
754
|
+
Both of those pick their category at runtime:
|
|
755
|
+
|
|
756
|
+
| Endpoint | Preferred category | Fallback |
|
|
757
|
+
| --- | --- | --- |
|
|
758
|
+
| `pipes/create` | `OST_PipeCurves` | `OST_GenericModel` |
|
|
759
|
+
| `sprinklers/place` | `OST_Sprinklers` | `OST_GenericModel` |
|
|
760
|
+
|
|
761
|
+
The preferred one is used **only if `DirectShape.IsValidCategoryId` accepts it in that document**.
|
|
762
|
+
Which categories can hold a `DirectShape` is Revit's own rule, it is not published, and it has
|
|
763
|
+
changed between releases — so the bridge asks rather than assumes, and **every row of the response
|
|
764
|
+
says which category the element actually ended up in**. Read it instead of guessing; a schedule of
|
|
765
|
+
the wrong category is an empty schedule. `pipes/create` takes an explicit `category` to override the
|
|
766
|
+
choice, and that one is validated the same way `directshape/create` validates its own.
|
|
767
|
+
|
|
768
|
+
Every element these four build can carry **`comments`** and **`mark`**, written to
|
|
769
|
+
`ALL_MODEL_INSTANCE_COMMENTS` and `ALL_MODEL_MARK` after creation and read straight back off the
|
|
770
|
+
element into the response. Per entry wins; a top-level `comments`/`mark` is the fallback for every
|
|
771
|
+
entry that does not carry its own. This is the difference between a "Mapa de quantidades
|
|
772
|
+
plantações" that reads `Planting: 38` and one that breaks down by species, because a schedule can
|
|
773
|
+
group by either parameter.
|
|
774
|
+
|
|
775
|
+
All four are one `TransactionGroup` for the whole batch with one inner transaction per entry, so a
|
|
776
|
+
solid Revit refuses rolls back alone and lands in `failed` with its `index`. The one qualification
|
|
777
|
+
is where the solids are built: `directshape/create` builds each entry's inside the per-entry catch,
|
|
778
|
+
while `planting/place`, `pipes/create` and `sprinklers/place` build theirs up front — so a malformed
|
|
779
|
+
run or a non-positive radius is a `BAD_REQUEST` for the whole call rather than one entry in
|
|
780
|
+
`failed`.
|
|
781
|
+
|
|
782
|
+
#### Every element gets a DirectShapeType
|
|
783
|
+
|
|
784
|
+
A `DirectShape` created by `DirectShape.CreateElement` alone has **no type at all**, and that is not
|
|
785
|
+
cosmetic: "Edit Type" in the Properties palette does nothing, the element cannot be scheduled or
|
|
786
|
+
filtered by type, and it has no type parameters to read or write. All four endpoints therefore
|
|
787
|
+
create the element's type as well — `DirectShapeType.Create(doc, name, categoryId)`, then
|
|
788
|
+
`DirectShape.SetTypeId` — and every created row reports the `typeId` and `typeName` it landed on.
|
|
789
|
+
|
|
790
|
+
The type name is `typeName` when the caller gives one; otherwise it follows the element's own
|
|
791
|
+
`name`, and failing that the category. That default is the useful one: a `planting/place` call whose
|
|
792
|
+
points already carry species names gets **a type per species** without asking for anything, and a
|
|
793
|
+
`directshape/create` with no names at all still gets one type named `Site` rather than none.
|
|
794
|
+
|
|
795
|
+
Types are **looked up before they are created**, by name within the category, both across calls and
|
|
796
|
+
within one: planting 140 trees of four species makes four types, not 140. The lookup is a
|
|
797
|
+
`FilteredElementCollector` over `DirectShapeType` the first time a name is seen in a call and a
|
|
798
|
+
dictionary hit after that, and the dictionary is only filled once the inner transaction has
|
|
799
|
+
committed — a type created in a transaction that then rolled back never existed, and caching its id
|
|
800
|
+
would hand the next entry a dead `ElementId`.
|
|
801
|
+
|
|
802
|
+
`SetTypeId` is called **before** `SetShape`, and once: the API documents it as settable a single
|
|
803
|
+
time, and the geometry wanted on the element is the instance's own. The type carries no shape.
|
|
804
|
+
|
|
805
|
+
#### Material comes from the solid, so it is an argument here
|
|
806
|
+
|
|
807
|
+
`materialId` (or `materialName`) on any of the four is applied through
|
|
808
|
+
`SolidOptions(materialId, graphicsStyleId)` handed to `GeometryCreationUtilities`, so every solid in
|
|
809
|
+
the batch is **built** carrying the material. There is no version of this that happens afterwards: a
|
|
810
|
+
`Solid`'s material is fixed at construction, which is why `materials/assign` refuses a `DirectShape`
|
|
811
|
+
and says what to do instead rather than quietly succeeding. The graphics style is
|
|
812
|
+
`InvalidElementId` — the caller asked for a material, not a subcategory.
|
|
813
|
+
|
|
814
|
+
When no material is named, the geometry is built through the **three-argument** overloads exactly as
|
|
815
|
+
it always was. Passing `SolidOptions` carrying `InvalidElementId` would probably mean the same
|
|
816
|
+
thing, and "probably" is not a reason to change how every existing call builds its geometry.
|
|
817
|
+
|
|
818
|
+
### Materials
|
|
819
|
+
|
|
820
|
+
`materials` reads, `materials/create` authors, `materials/set-texture` gives one a real bitmap (see
|
|
821
|
+
[Textures](#textures)), and `materials/assign` puts one on elements that already exist.
|
|
822
|
+
`walltypes/create` and `floortypes/create` are here too, because for a wall or a floor the material
|
|
823
|
+
is not a property of the element at all.
|
|
824
|
+
|
|
825
|
+
**There are two routes onto an element and they are not interchangeable:**
|
|
826
|
+
|
|
827
|
+
| The element | Where its material lives | How it gets one |
|
|
828
|
+
| --- | --- | --- |
|
|
829
|
+
| Anything from `directshape/create`, `planting/place`, `pipes/create`, `sprinklers/place` | on each `Solid`, fixed at construction | `materialId` on the endpoint that builds it |
|
|
830
|
+
| `Wall`, `Floor`, roof, ceiling — and `Toposolid`, whose `ToposolidType` is a `HostObjAttributes` too | on the **type**, in its `CompoundStructure` | `walltypes/create` / `floortypes/create`, then build with that type — or `materials/assign`, which edits the type |
|
|
831
|
+
| A family instance with a material parameter | on the instance | `materials/assign` |
|
|
832
|
+
|
|
833
|
+
`materials/create` sets `Color`, and `Transparency` (0–100) and `Shininess` (0–128) when they are
|
|
834
|
+
asked for — omitted, Revit's own defaults are left alone rather than overwritten with a zero nobody
|
|
835
|
+
asked for. It also sets **`UseRenderAppearanceForShading` to false** for a colour-only material:
|
|
836
|
+
that property is documented as the switch between "shaded views use the render appearance" and
|
|
837
|
+
"shaded views use `Color` and `Transparency`", and a material created for its colour is no use if
|
|
838
|
+
shaded views ignore it. A material given an `appearanceAssetId` gets `true` instead, because then
|
|
839
|
+
the asset is the point.
|
|
840
|
+
|
|
841
|
+
A name that already exists is **reused**, not overwritten: the response is `{"created": false}` plus
|
|
842
|
+
the material's **current** colour, transparency and shininess. Re-running the same call is therefore
|
|
843
|
+
safe, and a caller that gets back a colour it did not ask for is looking at a material somebody else
|
|
844
|
+
authored — worth knowing rather than trampling.
|
|
845
|
+
|
|
846
|
+
### Textures
|
|
847
|
+
|
|
848
|
+
`materials/set-texture` connects a real bitmap to a material's appearance asset, and
|
|
849
|
+
`materials/create`'s `texturePath` does the same thing at creation time. The route, all of it
|
|
850
|
+
verified against a live Revit 2027:
|
|
851
|
+
|
|
852
|
+
```
|
|
853
|
+
AppearanceAssetElement.Create(document, name, <library "Generic" asset>) // the asset to edit
|
|
854
|
+
using (Transaction) // REQUIRED, see below
|
|
855
|
+
using (AppearanceAssetEditScope scope)
|
|
856
|
+
Asset editable = scope.Start(assetElementId)
|
|
857
|
+
AssetProperty diffuse = editable.FindByName("generic_diffuse")
|
|
858
|
+
diffuse.AddConnectedAsset("UnifiedBitmap")
|
|
859
|
+
Asset bitmap = diffuse.GetSingleConnectedAsset()
|
|
860
|
+
bitmap.FindByName("unifiedbitmap_Bitmap") -> AssetPropertyString.Value = path
|
|
861
|
+
bitmap.FindByName("texture_RealWorldScaleX/Y") -> AssetPropertyDistance.Value = size
|
|
862
|
+
bitmap.FindByName("texture_WAngle") -> AssetPropertyDouble.Value = degrees
|
|
863
|
+
scope.Commit(true)
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**`AppearanceAssetEditScope.Commit` needs a transaction already open around it.** Without one it
|
|
867
|
+
throws `InvalidOperationException: EditScope cannot be closed, there is no opened transaction` —
|
|
868
|
+
and it throws it at `Commit`, after every edit has apparently succeeded, which is exactly the shape
|
|
869
|
+
of failure that looks like working code. `Start` succeeds either way and `scope.IsActive` is `true`
|
|
870
|
+
either way, so neither tells you. The scope lives inside `RevitWrite.InTransaction` for that reason.
|
|
871
|
+
|
|
872
|
+
**The schema problem, and the answer to it.** An asset's properties come from the schema it was
|
|
873
|
+
built from: `Generic` has `generic_diffuse`, `Ceramic` has `ceramic_color`, `MasonryCMU` has
|
|
874
|
+
`masonrycmu_color`, the Prism schemas have `opaque_albedo`, and `Water` and `Metal` have no
|
|
875
|
+
bitmap-bearing colour property at all. Editing whatever asset a material happens to carry is
|
|
876
|
+
therefore guesswork. Instead the material is **given** an asset created from Revit's own library
|
|
877
|
+
`Generic` asset — `Application.GetAssets(AssetType.Appearance)` returns ~3100 of them on a stock
|
|
878
|
+
2027 install, and the one whose `Name` is exactly `Generic` yields a full 58-property Generic asset
|
|
879
|
+
— unless the material already has a Generic asset that no other material shares. The report says
|
|
880
|
+
`assetAuthored` and `assetReason` so which of those happened is never a mystery. Only if the
|
|
881
|
+
library has no `Generic` to offer does it fall back to the asset in hand, trying each schema's
|
|
882
|
+
diffuse property in turn. Anything the asset turns out not to carry goes into `missing` rather than
|
|
883
|
+
throwing.
|
|
884
|
+
|
|
885
|
+
**Units.** `texture_RealWorldScaleX/Y` are `AssetPropertyDistance`, and on this machine
|
|
886
|
+
`GetUnitTypeId()` is `autodesk.unit.unit:inches` — a fresh bitmap defaults to `12`. `scale` is taken
|
|
887
|
+
in feet like every other length in this bridge and converted with `UnitUtils.ConvertFromInternalUnits`
|
|
888
|
+
rather than written raw, and `verified.scaleFeet` converts back so the caller reads what it asked
|
|
889
|
+
in. `texture_ScaleLock` ships `true`; an `x` and `y` that differ set it `false`, because different
|
|
890
|
+
sizes with the lock on is a state the UI would not let anyone author.
|
|
891
|
+
|
|
892
|
+
**Two side effects worth knowing.** Assigning a new appearance asset makes Revit repaint the
|
|
893
|
+
material's shading `Color` to match it — black, for a freshly created Generic one — so the colour
|
|
894
|
+
the material had is written back afterwards and reported in `colorRgb`: this endpoint textures a
|
|
895
|
+
material, it does not repaint it. And a material being textured gets
|
|
896
|
+
`UseRenderAppearanceForShading = true`, because a texture a shaded view ignores is not worth much.
|
|
897
|
+
|
|
898
|
+
**The bitmap is referenced, not embedded.** `unifiedbitmap_Bitmap` holds a path, so a path that does
|
|
899
|
+
not exist is an asset that renders nothing — `File.Exists` is checked up front and a miss is
|
|
900
|
+
`TEXTURE_NOT_FOUND` (404), before a material is created in the `materials/create` case. Revit ships
|
|
901
|
+
its own texture library under `C:\Program Files\Common Files\Autodesk Shared\Materials\Textures\`
|
|
902
|
+
(`1\Mats\`, `2\Mats\`, `3\Mats\` — around 5400 files: `grass_color.jpg`, `fieldstone_bump.jpg`,
|
|
903
|
+
`Finishes.Flooring.Wood.Plank.jpg`, `Sitework.Planting.Soil.jpg`, `water_calm.png`).
|
|
904
|
+
|
|
905
|
+
`materials/create`'s `texturePath` takes the bitmap only — scale, rotation and tint are
|
|
906
|
+
`materials/set-texture`'s business — and it does nothing for a material that already exists, which
|
|
907
|
+
still comes back untouched with `created: false` as it always has.
|
|
908
|
+
|
|
909
|
+
**What `appearanceAssetId` on `materials/create` still is:** the other half of this, and unchanged.
|
|
910
|
+
It **duplicates** an existing asset in the document (`AppearanceAssetElement.Duplicate`, which
|
|
911
|
+
duplicates the asset it holds) and assigns the copy, which is the documented way to give a new
|
|
912
|
+
material an existing material's rendered look without the two sharing one asset. `set-texture`
|
|
913
|
+
follows the same rule for the same reason: a shared asset is duplicated before it is edited, so one
|
|
914
|
+
material's texture can never appear on another's surfaces.
|
|
915
|
+
|
|
916
|
+
`materials/assign` takes the compound-structure route first for anything that has one — an element
|
|
917
|
+
that **is** a `HostObjAttributes` (a wall or floor type named directly) or a `HostObject` whose type
|
|
918
|
+
is one — and sets **every layer** of that structure. That changes every element of the type, which
|
|
919
|
+
is not what "assign to these elements" sounds like, so the row says `"appliesToType": true` and
|
|
920
|
+
names the type it edited. Otherwise it looks for a writable `ElementId` parameter, in order:
|
|
921
|
+
`STRUCTURAL_MATERIAL_PARAM`, `MATERIAL_ID_PARAM`, then a parameter literally called `Material`,
|
|
922
|
+
which is what a family author's own material parameter is usually called.
|
|
923
|
+
|
|
924
|
+
Everything else lands in `skipped` with a reason, and the reason is never a shrug. A `DirectShape`
|
|
925
|
+
gets the specific one — its material is carried by each solid and fixed when the solid was built, so
|
|
926
|
+
the fix is `materialId` on the endpoint that built it. One inner transaction per element, so an
|
|
927
|
+
element Revit refuses rolls back alone.
|
|
928
|
+
|
|
929
|
+
`walltypes/create` and `floortypes/create` **duplicate** an existing type — there is no
|
|
930
|
+
`WallType.Create` — and give the duplicate a single-layer `CompoundStructure`
|
|
931
|
+
(`CompoundStructure.CreateSingleLayerCompoundStructure(MaterialFunctionAssignment.Structure, width,
|
|
932
|
+
materialId)`). An omitted `thickness` keeps the source type's width and an omitted material keeps
|
|
933
|
+
the source's first-layer material, so either can be set without disturbing the other; with neither,
|
|
934
|
+
the call is a plain duplicate and the source's layering is left alone. A name that already exists is
|
|
935
|
+
reused with `{"created": false}` and is **not** re-cut to match the request — the response reports
|
|
936
|
+
the thickness and material the type really carries, read back off its structure. A source with no
|
|
937
|
+
compound structure at all (a curtain or stacked wall type) is a `BAD_REQUEST` naming it.
|
|
938
|
+
|
|
939
|
+
**The end cap condition is set per type, never inherited.** A `CompoundStructure` also carries an
|
|
940
|
+
`EndCap` — which shell layers wrap at the ends — and only a `WallType` may carry a real one.
|
|
941
|
+
`CreateSingleLayerCompoundStructure` hands back a wall's, so passing it straight to a `FloorType`
|
|
942
|
+
fails every time with `Input compound structure has wrong EndCap condition for this element type`.
|
|
943
|
+
Anything that is not a `WallType` therefore gets `EndCap = EndCapCondition.NoEndCap` before
|
|
944
|
+
`SetCompoundStructure`, which is the value the API documents as the one "floors and roofs must use".
|
|
945
|
+
It is **`NoEndCap`, not `None`**: `EndCapCondition.None` is a wall's "none of the shell layers
|
|
946
|
+
participate in end wrapping", still a wall-only condition, and a floor type rejects it just the
|
|
947
|
+
same. Walls keep exactly what Revit built, which is why `walltypes/create` never hit this. Any
|
|
948
|
+
future non-wall host type authored here — a ceiling, a roof, a `ToposolidType` — must say
|
|
949
|
+
`NoEndCap` too.
|
|
950
|
+
|
|
951
|
+
Then `walls/create` takes the new type by name — as `wallType`, or as `typeName`, which is what
|
|
952
|
+
every other type-taking endpoint here calls it — and `floors/create` already took `typeName`, so a
|
|
953
|
+
type authored this way is selected by name with no further work.
|
|
954
|
+
|
|
955
|
+
### Units
|
|
956
|
+
|
|
957
|
+
**Revit internal units — decimal feet — in both directions, unconverted.** Elevations, heights and
|
|
958
|
+
coordinates are all raw API values. The bridge deliberately converts nothing, so `levels` and
|
|
959
|
+
`levels/create` are symmetric.
|
|
960
|
+
|
|
961
|
+
The one exception is `materials/set-texture`'s `scale`, and it exists to keep that promise rather
|
|
962
|
+
than break it: an appearance asset's `texture_RealWorldScaleX/Y` is an `AssetPropertyDistance` in
|
|
963
|
+
**inches**, not feet, so the value is converted on the way in and back again in `verified.scaleFeet`.
|
|
964
|
+
A caller still speaks feet everywhere.
|
|
965
|
+
|
|
966
|
+
### Errors
|
|
967
|
+
|
|
968
|
+
Every failure, at any status code, uses one shape:
|
|
969
|
+
|
|
970
|
+
```json
|
|
971
|
+
{ "error": { "code": "NO_ACTIVE_DOCUMENT", "message": "Revit has no active document...", "stack": "..." } }
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
`message` and `stack` are the contract the Node client reads. `code` is an additive extra so a tool
|
|
975
|
+
can branch on a condition without string-matching prose.
|
|
976
|
+
|
|
977
|
+
| Status | `code` | When |
|
|
978
|
+
| --- | --- | --- |
|
|
979
|
+
| 400 | `BAD_REQUEST`, `BAD_JSON` | Malformed body, missing or wrong-typed field, unknown category/level/wall/floor/toposolid type, unknown material, a colour channel outside 0-255 or a transparency/shininess outside Revit's range, unknown `detailing` or `kind` |
|
|
980
|
+
| 404 | `UNKNOWN_PATH`, `UNKNOWN_ENDPOINT`, `ELEMENT_NOT_FOUND`, `PARAMETER_NOT_FOUND`, `TEMPLATE_NOT_FOUND`, `FILE_NOT_FOUND`, `TEXTURE_NOT_FOUND`, `GENERIC_ASSET_UNAVAILABLE` | No such route, no such element, no template, no file at the path `document/open` was given, no file at the `texturePath` a material was to be textured with, or no `Generic` asset in Revit's library to build an appearance from |
|
|
981
|
+
| 405 | `METHOD_NOT_ALLOWED` | Anything that is not POST |
|
|
982
|
+
| 409 | `NO_ACTIVE_DOCUMENT`, `NOT_A_PROJECT_DOCUMENT`, `FILE_EXISTS`, `FILE_LOCKED`, `LAST_DOCUMENT_CANNOT_CLOSE`, `NO_TITLEBLOCK`, `NOT_SAVEABLE`, `NO_VIEW_FAMILY_TYPE`, `NO_TEXT_NOTE_TYPE`, `NO_TOPOSOLID`, `VIEW_ALREADY_PLACED`, `CANNOT_PLACE`, `CANNOT_DUPLICATE`, `VIEW_CANNOT_HOST_DETAIL`, `VIEW_CANNOT_HOST_TEXT` | No document open, a family document where a project is required, a path that already exists, a file an overwrite could not delete because something still holds it, the only open document asked to close with no stand-in Revit would accept, no title block family loaded, a `document/save` on a model that has never been saved, no floor-plan/section/drafting view family type or no default text type in the template, no toposolid to flatten, a view already on a sheet, a view Revit will not put on that sheet, a view Revit will not duplicate that way, a view that is not a drafting view or a plan asked for detail lines, or a schedule/sheet/template asked for text |
|
|
983
|
+
| 500 | `REVIT_API_ERROR`, `INTERNAL_ERROR`, `RELOAD_FAILED` | The Revit API threw, an unexpected bug, or the handlers assembly could not be reloaded (the previous one is still serving) |
|
|
984
|
+
| 503 | `REVIT_BUSY`, `BRIDGE_NOT_READY`, `BRIDGE_SHUTTING_DOWN` | Revit did not pick the work up in time, or the handlers assembly is not loaded |
|
|
985
|
+
|
|
986
|
+
**No active document returns a structured 409, never a NullReferenceException.** It is the second
|
|
987
|
+
most common real-world failure — a request arrives while Revit sits on the start page or between
|
|
988
|
+
documents. The guard is **not central**: every endpoint opts in by calling
|
|
989
|
+
`RevitFacts.RequireDocument`. `status` does not, since it is how a client discovers the fact, and
|
|
990
|
+
neither does `document/new`, which has to work on a Revit with nothing open.
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## Why the ExternalEvent pump exists
|
|
995
|
+
|
|
996
|
+
This is the part of the design that is not negotiable, so it is worth understanding before changing
|
|
997
|
+
anything in `RevitApiContext`.
|
|
998
|
+
|
|
999
|
+
**Revit API calls are only legal on Revit's main thread, from inside an event Revit itself raises.**
|
|
1000
|
+
`HttpListener` hands requests to arbitrary thread-pool threads. Touching a `Document` from one of
|
|
1001
|
+
those is undefined behaviour: usually an immediate "attempting to modify the model outside of a
|
|
1002
|
+
transaction" style exception, sometimes a hard crash that takes the user's unsaved model with it.
|
|
1003
|
+
|
|
1004
|
+
So every request crosses the thread boundary through a single pump:
|
|
1005
|
+
|
|
1006
|
+
```
|
|
1007
|
+
listener thread Revit main thread
|
|
1008
|
+
--------------- -----------------
|
|
1009
|
+
enqueue work item
|
|
1010
|
+
externalEvent.Raise() ───────────────► Execute(UIApplication)
|
|
1011
|
+
block on ManualResetEventSlim drain queue, run each item,
|
|
1012
|
+
(default 30 s) capture result OR exception
|
|
1013
|
+
◄─────────────────────────────── signal the item
|
|
1014
|
+
read result / rethrow
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
Details that are load-bearing:
|
|
1018
|
+
|
|
1019
|
+
- **It is a queue, not a single slot.** Two concurrent requests must not clobber each other's
|
|
1020
|
+
result.
|
|
1021
|
+
- **Exceptions are captured and rethrown** on the calling thread via `ExceptionDispatchInfo`, so the
|
|
1022
|
+
original stack survives into the `stack` field of the error response.
|
|
1023
|
+
- **On timeout the bridge answers `503`**, not a hang and not a generic `500`. This is the single
|
|
1024
|
+
most common real-world failure: Revit is showing a modal dialog, so the main thread never becomes
|
|
1025
|
+
idle and never runs our handler. The message says so explicitly, because "request timed out" sends
|
|
1026
|
+
people looking in the wrong place.
|
|
1027
|
+
- **A timed-out work item is marked abandoned and skipped** rather than executed later. Silently
|
|
1028
|
+
mutating the model after the client has already been told the request failed would be the worst
|
|
1029
|
+
possible surprise. If the item had *already started* when the timeout fired it cannot be
|
|
1030
|
+
cancelled — a Revit API call is not interruptible — so the 503 message says the operation may
|
|
1031
|
+
still complete inside Revit.
|
|
1032
|
+
- **Accept and handle are separate threads.** The accept loop hands each request to the thread pool,
|
|
1033
|
+
so one request parked on the pump does not stall the listener for everybody else.
|
|
1034
|
+
|
|
1035
|
+
### Transactions
|
|
1036
|
+
|
|
1037
|
+
Every write endpoint wraps its work in a **`TransactionGroup` that is `Assimilate()`d on success and
|
|
1038
|
+
`RollBack()`ed on failure**. That is what makes **one request equal exactly one Ctrl+Z**, and what
|
|
1039
|
+
guarantees a request failing half way through leaves no partial edit behind. `parameters/set` over
|
|
1040
|
+
five elements is all-or-nothing. This is a requirement of the bridge, not an optimisation — do not
|
|
1041
|
+
"simplify" a write endpoint down to a bare `Transaction`.
|
|
1042
|
+
|
|
1043
|
+
The group is also what makes a *partially* skippable batch possible: `sheets/create` opens one inner
|
|
1044
|
+
`Transaction` per sheet inside the one group, so the sheet whose number Revit refused rolls back
|
|
1045
|
+
alone while the other 28 still assimilate into a single undo entry.
|
|
1046
|
+
|
|
1047
|
+
The `document/*` endpoints are the exception, and deliberately so: creating, saving, closing and
|
|
1048
|
+
opening a document are not transactable model edits, and Revit throws if they are attempted inside a
|
|
1049
|
+
transaction. That is why they live in `DocumentEndpoints` rather than in `WriteEndpoints`.
|
|
1050
|
+
|
|
1051
|
+
Every transaction opened through `RevitWrite.InTransaction` also gets the bridge's
|
|
1052
|
+
`IFailuresPreprocessor` — see the next section. That is wired in centrally, in one place, precisely
|
|
1053
|
+
so no endpoint can forget it.
|
|
1054
|
+
|
|
1055
|
+
---
|
|
1056
|
+
|
|
1057
|
+
## Unattended operation
|
|
1058
|
+
|
|
1059
|
+
The bridge is meant to be driven with nobody at the keyboard, and that takes more than an HTTP
|
|
1060
|
+
listener. Revit stops and asks. A modal dialog parks the main thread inside its own message loop:
|
|
1061
|
+
the `ExternalEvent` pump never runs, and every request from that moment on answers `REVIT_BUSY`
|
|
1062
|
+
until a human clicks a button. A commit-time warning does the same thing from inside the bridge's
|
|
1063
|
+
own write.
|
|
1064
|
+
|
|
1065
|
+
Two pieces deal with it, and one exists to keep them honest.
|
|
1066
|
+
|
|
1067
|
+
**`BridgeDialogWatcher`** subscribes to `UIControlledApplication.DialogBoxShowing` and answers with
|
|
1068
|
+
`OverrideResult`: `1` (IDOK / `TaskDialogResult.Ok`) for an ordinary dialog, `2` (IDCANCEL) for
|
|
1069
|
+
anything whose id or message text matches a destructive-sounding word — delete, remove, unload,
|
|
1070
|
+
discard, overwrite, replace, purge, save, close, erase, detach, relinquish. The heuristic is crude
|
|
1071
|
+
on purpose: cancelling a harmless dialog costs one failed request, OK-ing a destructive one costs
|
|
1072
|
+
the user's model. It is subscribed **after** the listener starts, so the bridge's own "did not
|
|
1073
|
+
start" dialogs are still shown to the user rather than clicked away unseen.
|
|
1074
|
+
|
|
1075
|
+
**`BridgeFailuresPreprocessor`** is installed on every transaction `RevitWrite` opens. In order: a
|
|
1076
|
+
failure that offers a resolution gets `ResolveFailure`; otherwise a warning gets `DeleteWarning`;
|
|
1077
|
+
otherwise — an error with no resolution — it is left alone, because `DeleteWarning` refuses anything
|
|
1078
|
+
that is not a warning and suppressing an error would let a broken edit commit. Revit then fails the
|
|
1079
|
+
transaction and the whole `TransactionGroup` rolls back, exactly as it would have.
|
|
1080
|
+
|
|
1081
|
+
**`BridgeDiagnostics`** is the honesty half. Suppressed is not swallowed: every dismissed dialog
|
|
1082
|
+
(id, message, the result used) and every resolved warning (transaction name, severity, text, what
|
|
1083
|
+
was done) goes into a ring buffer of 200 entries each, served by `diagnostics`. Entries evicted past
|
|
1084
|
+
the cap are counted in `dropped`, so a truncated buffer never reads as a quiet one, and every entry
|
|
1085
|
+
is also written to `bridge.log` with the same timestamp format so the two can be lined up.
|
|
1086
|
+
|
|
1087
|
+
A caller that writes **must** read `diagnostics` afterwards. A silently resolved warning is often
|
|
1088
|
+
the model doing something the caller never asked for; without reading them, the bridge's convenience
|
|
1089
|
+
would be indistinguishable from data loss. `diagnostics/config` takes `{"autoDismiss": false}` to
|
|
1090
|
+
hand the dialogs back to a human — the right setting when somebody is working in Revit at the same
|
|
1091
|
+
time — and `{"clear": true}` to empty the buffer, which is worth doing before a batch so the read
|
|
1092
|
+
afterwards covers only that batch.
|
|
1093
|
+
|
|
1094
|
+
Dialogs seen while auto-dismiss is **off** are recorded too, with `"answered": false`. That is the
|
|
1095
|
+
one thing a caller staring at `REVIT_BUSY` most needs to know.
|
|
1096
|
+
|
|
1097
|
+
---
|
|
1098
|
+
|
|
1099
|
+
## Hot reload
|
|
1100
|
+
|
|
1101
|
+
The add-in is two assemblies:
|
|
1102
|
+
|
|
1103
|
+
```
|
|
1104
|
+
RevitMcpBridge.dll the loader. Revit pins it for the session.
|
|
1105
|
+
IExternalApplication, HttpListener, ExternalEvent pump,
|
|
1106
|
+
dialog watcher, failures preprocessor, diagnostics, JSON.
|
|
1107
|
+
RevitMcpBridge.Handlers.dll the reloadable logic: the route table and every endpoint.
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
`HandlerAssembly` shadow-copies the handlers DLL to `%LOCALAPPDATA%\RevitMcpBridge\shadow\<stamp>\`
|
|
1111
|
+
and loads it there into a **collectible** `AssemblyLoadContext`. POSTing to `reload` loads a freshly
|
|
1112
|
+
built one, swaps it in, and retires the old context. The model stays open, the listener keeps its
|
|
1113
|
+
port, queued work is untouched — only the routing and endpoint logic changes.
|
|
1114
|
+
|
|
1115
|
+
The rules that make it work, each of which is load-bearing:
|
|
1116
|
+
|
|
1117
|
+
- **The loader never references the handlers assembly at compile time.** The dependency runs the
|
|
1118
|
+
other way: the handlers project references the loader, implements `IBridgeRouter` (declared in the
|
|
1119
|
+
loader) and is found by type name at runtime. A compile-time reference would bind the handlers in
|
|
1120
|
+
the default load context and nothing could ever unload. `InternalsVisibleTo` is what lets the two
|
|
1121
|
+
halves share `BridgeException`, `JsonBody` and the rest without going public.
|
|
1122
|
+
- **It is loaded from a shadow copy.** A loaded file is locked, and a locked file cannot be
|
|
1123
|
+
overwritten by the rebuild the reload exists to pick up. Old shadow directories are pruned
|
|
1124
|
+
best-effort; one still in use simply stays until a later start.
|
|
1125
|
+
- **The load context resolves nothing itself** (`Load` returns `null`), so the loader assembly, the
|
|
1126
|
+
Revit API and the framework all bind to the copies already loaded in the default context. Two
|
|
1127
|
+
copies of `RevitAPI` would break type identity in the most confusing way available.
|
|
1128
|
+
- **`reload` is answered by the loader, before the router is consulted.** It has to be: a router
|
|
1129
|
+
unloading the context its own method is executing in would be sawing the branch it stands on.
|
|
1130
|
+
Measured outside Revit — a frame still holding a reference to collectible code reliably prevents
|
|
1131
|
+
the unload from completing.
|
|
1132
|
+
- **Whether the old context went is measured, not assumed.** `Retire` nulls every field, calls
|
|
1133
|
+
`Unload`, and hands back a `WeakReference`; `WaitForUnload` collects a few times and reports what
|
|
1134
|
+
the `WeakReference` says. If something is still holding the old context — most likely a request
|
|
1135
|
+
that was still running — the response comes back with `"unloadedPrevious": false` and a warning
|
|
1136
|
+
saying the new logic is live but the old context leaked. That is deliberately not smoothed over.
|
|
1137
|
+
- **If the collectible load fails for any reason, the bridge falls back** to loading the handlers in
|
|
1138
|
+
the default context. It then works exactly as before but cannot hot-reload, `reload` says so in
|
|
1139
|
+
`collectible` and `warning`, and the reason is in `bridge.log`. A bridge that works but cannot
|
|
1140
|
+
reload beats a bridge that will not start.
|
|
1141
|
+
|
|
1142
|
+
What is **not** verified: all of the above was exercised outside Revit, against a stub handlers
|
|
1143
|
+
assembly — load, call, shadow-copy-while-loaded, unload, and a deliberate leak to confirm the check
|
|
1144
|
+
detects one. Whether Revit itself ends up holding a reference into the collectible context in some
|
|
1145
|
+
situation is not something a harness can answer. If `unloadedPrevious` comes back `false` on every
|
|
1146
|
+
reload, that is the signal it does.
|
|
1147
|
+
|
|
1148
|
+
---
|
|
1149
|
+
|
|
1150
|
+
## Why it binds 127.0.0.1 and not `+`
|
|
1151
|
+
|
|
1152
|
+
The listener prefix is the **`127.0.0.1` literal**, never `http://+:48884/` or `http://*:48884/`.
|
|
1153
|
+
|
|
1154
|
+
1. **`+` and `*` are "strong" wildcard prefixes and http.sys refuses them to a non-elevated
|
|
1155
|
+
process** unless an administrator registered a URL ACL first (`netsh http add urlacl`). Revit
|
|
1156
|
+
normally runs unelevated, so a wildcard prefix means the bridge just fails to start with
|
|
1157
|
+
*Access is denied* on most machines. Measured on this machine, unelevated:
|
|
1158
|
+
`http://127.0.0.1:48884/` starts fine, while `http://+:48885/` fails with
|
|
1159
|
+
`HttpListenerException ErrorCode=5`.
|
|
1160
|
+
2. **A wildcard binds every interface**, which publishes an unauthenticated remote-control API for
|
|
1161
|
+
the user's models onto the LAN. The loopback literal needs no admin URL ACL and is simply not
|
|
1162
|
+
reachable off-machine.
|
|
1163
|
+
|
|
1164
|
+
There is a comment saying exactly this in `HttpBridgeServer.cs` so nobody "helpfully" changes it.
|
|
1165
|
+
|
|
1166
|
+
The listener takes the whole port on loopback and the router enforces the `/revit-mcp` base path
|
|
1167
|
+
itself, so an unmatched path returns a structured JSON 404 instead of the raw http.sys 400 that a
|
|
1168
|
+
narrower prefix would produce.
|
|
1169
|
+
|
|
1170
|
+
### Port already in use
|
|
1171
|
+
|
|
1172
|
+
Only one process can own the port, so a second Revit instance with the bridge installed will fail to
|
|
1173
|
+
bind. That is handled: the add-in **logs it, writes a Revit journal comment, shows a `TaskDialog`,
|
|
1174
|
+
and leaves Revit running normally** with the bridge inert for that session. It never takes Revit
|
|
1175
|
+
down. `HttpListener` reports this as `ErrorCode=183` (verified) or `32`, both of which get a message
|
|
1176
|
+
naming the likely cause.
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## Configuration
|
|
1181
|
+
|
|
1182
|
+
| Variable | Default | Effect |
|
|
1183
|
+
| --- | --- | --- |
|
|
1184
|
+
| `REVIT_MCP_BRIDGE_TIMEOUT_MS` | `30000` | How long a request waits for Revit's main thread. Clamped to 1 s – 600 s. |
|
|
1185
|
+
|
|
1186
|
+
Per-request `timeoutMs` in the body overrides it. The port is fixed at 48884 by design.
|
|
1187
|
+
|
|
1188
|
+
Dialog auto-dismiss has no environment variable on purpose: it starts **on** and is changed through
|
|
1189
|
+
`diagnostics/config` at runtime, because whether a human is sitting in front of Revit is not a
|
|
1190
|
+
property of how the process was started.
|
|
1191
|
+
|
|
1192
|
+
**Log file:** `%LOCALAPPDATA%\RevitMcpBridge\bridge.log` (truncated past 1 MB). It is under
|
|
1193
|
+
LocalAppData deliberately — Windows Controlled Folder Access blocks writes to Documents and Pictures
|
|
1194
|
+
and surfaces them as misleading IO errors.
|
|
1195
|
+
|
|
1196
|
+
---
|
|
1197
|
+
|
|
1198
|
+
## Layout
|
|
1199
|
+
|
|
1200
|
+
```
|
|
1201
|
+
revit-bridge/
|
|
1202
|
+
RevitMcpBridge.csproj the loader. net8.0-windows, x64, reference-only Revit API.
|
|
1203
|
+
Builds handlers\ after itself, into the same output folder.
|
|
1204
|
+
install.ps1 detect / install / uninstall, PS 5.1 compatible
|
|
1205
|
+
dist/ prebuilt add-in that ships on npm (committed) - BOTH assemblies
|
|
1206
|
+
src/ the loader assembly: everything that survives a reload
|
|
1207
|
+
BridgeApplication.cs IExternalApplication entry point (thin, and why)
|
|
1208
|
+
BridgeHost.cs owns the pump, the server, the dialog watcher, the handler host
|
|
1209
|
+
RevitApiContext.cs ExternalEvent pump: worker thread <-> Revit main thread
|
|
1210
|
+
HttpBridgeServer.cs HttpListener, 127.0.0.1 binding, port-in-use handling, reload route
|
|
1211
|
+
HandlerHost.cs which generation of the handlers assembly is current; reload
|
|
1212
|
+
HandlerAssembly.cs collectible AssemblyLoadContext, shadow copy, verified unload
|
|
1213
|
+
IBridgeRouter.cs the one thing the loader knows about the reloadable half
|
|
1214
|
+
BridgeDialogWatcher.cs answers Revit's modal dialogs (unattended operation)
|
|
1215
|
+
BridgeFailures.cs IFailuresPreprocessor: resolves commit-time warnings
|
|
1216
|
+
BridgeDiagnostics.cs ring buffers of what was suppressed; the auto-dismiss switch
|
|
1217
|
+
BridgeJson.cs serializer options + the one error shape
|
|
1218
|
+
JsonBody.cs request-body readers
|
|
1219
|
+
BridgeLog.cs rolling log file
|
|
1220
|
+
BridgeException.cs expected failures with an HTTP status + code
|
|
1221
|
+
BridgeInfo.cs assembly version (of either half)
|
|
1222
|
+
AssemblyInfo.cs InternalsVisibleTo the handlers assembly
|
|
1223
|
+
handlers/ the reloadable assembly: routing + endpoints
|
|
1224
|
+
RevitMcpBridge.Handlers.csproj
|
|
1225
|
+
src/
|
|
1226
|
+
RequestRouter.cs route table, JSON in/out, error mapping; implements IBridgeRouter
|
|
1227
|
+
Endpoints/
|
|
1228
|
+
ReadEndpoints.cs status, levels, categories, query, elements, selection, titleblocks, sheets
|
|
1229
|
+
WriteEndpoints.cs levels/create, walls/create, parameters/set, elements/delete, sheets/create
|
|
1230
|
+
ModelEndpoints.cs toposolid/create, floors/create, families/load, families/symbols,
|
|
1231
|
+
families/place, openings/place, walltypes/create, floortypes/create
|
|
1232
|
+
MaterialEndpoints.cs materials, materials/create, materials/set-texture,
|
|
1233
|
+
materials/assign
|
|
1234
|
+
ViewEndpoints.cs views, views/create-plan, views/create-section, views/create-3d,
|
|
1235
|
+
views/set-style, views/set-background, views/hide-categories,
|
|
1236
|
+
views/set-sun, views/export-image, views/duplicate,
|
|
1237
|
+
sheets/place-view, schedules/create
|
|
1238
|
+
DirectShapeEndpoints.cs directshape/create, planting/place, pipes/create, sprinklers/place -
|
|
1239
|
+
geometry with no family behind it, typed and with a material
|
|
1240
|
+
DocumentEndpoints.cs document/new, open, save, save-as, close - never transacted
|
|
1241
|
+
DiagnosticsEndpoints.cs diagnostics, diagnostics/config
|
|
1242
|
+
RevitWrite.cs TransactionGroup helper (one request == one Ctrl+Z) + failures wiring
|
|
1243
|
+
RevitFacts.cs document access, lookups, compact-row shape, point/loop readers
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
### Building both halves
|
|
1247
|
+
|
|
1248
|
+
`dotnet build RevitMcpBridge.csproj -c Release` produces **both** assemblies into
|
|
1249
|
+
`bin\Release\net8.0-windows\`. The handlers project is not a `ProjectReference` — it references the
|
|
1250
|
+
loader, so a `ProjectReference` back would be a cycle — and is built instead by a `BuildHandlers`
|
|
1251
|
+
target that runs `Restore;Build` on it after the loader is compiled. That is why one build command
|
|
1252
|
+
still gives you a complete add-in, and why `install.ps1` (which copies every file it finds next to
|
|
1253
|
+
`RevitMcpBridge.dll`) needs no special knowledge of the split.
|
|
1254
|
+
|
|
1255
|
+
Shipping one half without the other is a bridge that does not start, so `scripts/build-bridge.mjs`
|
|
1256
|
+
requires all four files (both DLLs and both `.deps.json`) and fails the build if one is missing.
|