|
| 1 | +# Rhino → Revit, start to finish |
| 2 | + |
| 3 | +You are an assistant. Somebody is modelling in Rhino for a project whose model lives in Revit, and |
| 4 | +they want what they draw to become real Revit elements. This is the whole path, in the order it |
| 5 | +happens, with the numbers from a run that actually worked. |
| 6 | + |
| 7 | +It assumes the collaboration side is already set up — a central published, a project on the |
| 8 | +platform. If it is not, that is |
| 9 | +**[the Revit collaboration quick start](https://raw.githubusercontent.com/ThatOpen/platform_services/main/docs/revit-collab-quickstart.md)**, and it comes first. |
| 10 | + |
| 11 | +## What is actually happening |
| 12 | + |
| 13 | +Five steps, and it is worth saying them plainly before touching anything, because the shape is what |
| 14 | +people get wrong: |
| 15 | + |
| 16 | +1. Rhino **publishes** its model to the platform as a `.frag`. That is all the plug-in does. It |
| 17 | + mentions Revit nowhere. |
| 18 | +2. A **converter** — code in the user's own app, not in either plug-in — reads that `.frag` and |
| 19 | + decides what it means in Revit's vocabulary. |
| 20 | +3. The converter **queues a proposal**. It changes nothing. |
| 21 | +4. Somebody **in Revit accepts it**. Applying is a decision, and the decision belongs to the person |
| 22 | + whose model it is. |
| 23 | +5. They **sync**, and it becomes a commit like any other. |
| 24 | + |
| 25 | +**No element is ever tied to an element.** The only thing linking a Rhino object to a Revit column |
| 26 | +is the converter's own note of what it asked for. Anybody proposing to store a link between the two |
| 27 | +models is proposing the thing this architecture exists to avoid. |
| 28 | + |
| 29 | +## Before anything |
| 30 | + |
| 31 | +Log in: **[Logging in to That Open Platform](https://raw.githubusercontent.com/ThatOpen/platform_services/main/docs/platform-token.md)**. The Rhino plug-in reads |
| 32 | +`~/.thatopen/config.json`, the same file the CLI writes. It deliberately does not read the Revit |
| 33 | +add-in's session — same machine, but one plug-in reading another's private state is coupling nobody |
| 34 | +can undo later. |
| 35 | + |
| 36 | +Install the plug-in. **Rhino has to be closed**; the installer holds its files. |
| 37 | + |
| 38 | +```bash |
| 39 | +thatopen rhino install |
| 40 | +``` |
| 41 | + |
| 42 | +## Driving Rhino without a keyboard |
| 43 | + |
| 44 | +The plug-in runs a local HTTP server, exactly like the Revit add-in. This is what lets you do the |
| 45 | +whole job while the user watches, instead of dictating commands for them to type. |
| 46 | + |
| 47 | +The port and the token are written to `%APPDATA%\ThatOpen\rhino-addin.json`: |
| 48 | + |
| 49 | +```json |
| 50 | +{ "port": 5620, "token": "…", "pid": 5436 } |
| 51 | +``` |
| 52 | + |
| 53 | +Every request is a POST with a JSON body and the header `X-RhinoFlow-Token`. **Never print the |
| 54 | +token**, and never write it into a script you are about to show. |
| 55 | + |
| 56 | +There is also a `script` endpoint that runs Rhino's own command line. Use it for drawing, which is |
| 57 | +what it is good at. **Never use it for a command that asks a question** — the import command wants |
| 58 | +the project id the first time it runs, and a scripted command has nobody to answer, so Rhino sits |
| 59 | +there with no error and no progress. That is why `import-model` and `publish` take their answers as |
| 60 | +parameters instead of being reachable through `script`. |
| 61 | + |
| 62 | +## 1. Bring the team's model into Rhino |
| 63 | + |
| 64 | +``` |
| 65 | +POST /import-model { "project": "<projectId>", "document": "<doc>" } |
| 66 | +``` |
| 67 | + |
| 68 | +```json |
| 69 | +{ "ok": true, "model": "rstadvancedsampleproject.frag", |
| 70 | + "layer": "ThatOpen reference — rstadvancedsampleproject", |
| 71 | + "added": 907, "skipped": 0 } |
| 72 | +``` |
| 73 | + |
| 74 | +It lands on a **locked layer**, with no properties, and it cannot be sent back. Say why if the user |
| 75 | +asks, because it looks like a limitation and it is a decision: a `.frag` is a tessellation — what a |
| 76 | +Revit wall LOOKS like, not what it IS — so editing one in Rhino and returning it could only produce |
| 77 | +a worse version of a model Revit already owns. The lock stops somebody editing a mesh that is really |
| 78 | +somebody else's wall and then proposing that as a change. |
| 79 | + |
| 80 | +If the folder holds more than one model the call comes back with `available` listing them and no |
| 81 | +import. Ask which; do not pick the first. Somebody spending an afternoon modelling against the wrong |
| 82 | +building is a bad afternoon. |
| 83 | + |
| 84 | +## 2. Find out where things are, before you place anything |
| 85 | + |
| 86 | +Do not invent coordinates. Ask the model: |
| 87 | + |
| 88 | +``` |
| 89 | +POST /objects { "limit": 3000 } |
| 90 | +``` |
| 91 | + |
| 92 | +Each object comes back with `min`, `max`, `center`, its layer, and the user strings the import |
| 93 | +stamped on it — including `ThatOpen:Category`, which is Revit's own category name. |
| 94 | + |
| 95 | +**Group before you conclude.** Reading the first few rows of that list is not measuring, and it will |
| 96 | +mislead you. Measured on the sample project: the first four columns in the list were 750×750, based |
| 97 | +at Z −2500, 2500 tall. They were the basement — four out of seventy. The building's actual column is |
| 98 | +**450×450, based at Z 0, 3500 tall**, which is 63 of the 70. Taking the section from the first rows |
| 99 | +produced columns that were too fat and buried, twice. |
| 100 | + |
| 101 | +The same query gives you the grid. Sort each row of columns by X, look at the gaps, and the midpoint |
| 102 | +of a wide one is a defensible place for a new column — it sits under the beam spanning that bay. |
| 103 | + |
| 104 | +## 3. Place the columns |
| 105 | + |
| 106 | +There is no "make me a column" endpoint, and there should not be. This plug-in moves data and |
| 107 | +decides nothing about what any of it means; the moment it grows a command called `columns` it has an |
| 108 | +opinion about buildings, and that opinion belongs in the converter. Rhino's own commands do the job: |
| 109 | + |
| 110 | +``` |
| 111 | +POST /script { "script": "_-Layer _New \"flow:columns\" _Current \"flow:columns\" _Enter" } |
| 112 | +POST /script { "script": "_-Point -5544,-1413,0" } |
| 113 | +POST /script { "script": "_-Box -5769,-1638,0 -5319,-1188,0 3500" } |
| 114 | +``` |
| 115 | + |
| 116 | +Verified: that produces a point at the given coordinate and a 450×450×3500 extrusion centred on it. |
| 117 | +`_-Box` takes two opposite corners of the base and then a height, so a 450 square centred on |
| 118 | +`(x, y)` runs from `(x−225, y−225)` to `(x+225, y+225)`. |
| 119 | + |
| 120 | +**Place BOTH, and understand why.** The converter reads **points** on a layer and turns each into a |
| 121 | +Revit column. It does not read solids. But a point is invisible at building scale, so a Rhino file of |
| 122 | +"the new columns" looks like an empty file — impossible to work with and impossible to show anybody. |
| 123 | +So the point is the data, and the box is the picture. Put them on sibling layers (`flow:columns` and |
| 124 | +`flow:columns (view)`) so the converter's layer holds nothing but points. |
| 125 | + |
| 126 | +Nothing downstream reads the box. If somebody later rewrites the converter to read solids, the point |
| 127 | +layer goes away and everything else still works. |
| 128 | + |
| 129 | +**Sizes and coordinates are in the DOCUMENT's units.** `POST /status` reports them. The sample |
| 130 | +project is in millimetres, where a column of `0.45` is an invisible splinter. Converting from metres |
| 131 | +behind the user's back would put everything a thousand times too far away. |
| 132 | + |
| 133 | +### The whole thing, worked |
| 134 | + |
| 135 | +Four columns down one bay of the sample project, at the midpoints of a 7315 mm gap that repeats row |
| 136 | +after row. This is exactly what was run: |
| 137 | + |
| 138 | +``` |
| 139 | +_-Layer _New "flow:columns" _Enter |
| 140 | +_-Layer _New "flow:columns (view)" _Enter |
| 141 | +
|
| 142 | +_-Layer _Current "flow:columns" _Enter |
| 143 | +_-Point -5544,-5769,0 |
| 144 | +_-Point -5544,-1413,0 |
| 145 | +_-Point -5544,530,0 |
| 146 | +_-Point -5544,6829,0 |
| 147 | +
|
| 148 | +_-Layer _Current "flow:columns (view)" _Enter |
| 149 | +_-Box -5769,-5994,0 -5319,-5544,0 3500 |
| 150 | +_-Box -5769,-1638,0 -5319,-1188,0 3500 |
| 151 | +_-Box -5769,305,0 -5319,755,0 3500 |
| 152 | +_-Box -5769,6604,0 -5319,7054,0 3500 |
| 153 | +``` |
| 154 | + |
| 155 | +One `POST /script` per line, or several lines in one call. Check `objectsAfter` in the reply: it is |
| 156 | +the only thing that tells a command that did nothing from one that worked. |
| 157 | + |
| 158 | +### And then anything else |
| 159 | + |
| 160 | +Nothing above is about columns. It is: make a layer, put geometry on it, and let the converter say |
| 161 | +what it means. The same shape covers whatever the user actually wants — |
| 162 | + |
| 163 | +| want | command | |
| 164 | +| --- | --- | |
| 165 | +| a point | `_-Point x,y,z` | |
| 166 | +| a box | `_-Box x1,y1,z1 x2,y2,z2 <height>` | |
| 167 | +| a line | `_-Line x1,y1,z1 x2,y2,z2` | |
| 168 | +| a circle | `_-Circle x,y,z <radius>` | |
| 169 | +| a layer, made current | `_-Layer _New "name" _Current "name" _Enter` | |
| 170 | + |
| 171 | +The leading `-` is what matters: it is Rhino's command-line form, which takes its arguments inline |
| 172 | +instead of opening a dialog. Without it the command puts a window on screen and waits for a person, |
| 173 | +and there is no person. |
| 174 | + |
| 175 | +Two rules carry over to anything you draw here. The geometry the converter reads goes on **its own |
| 176 | +layer, holding nothing else**, because the converter selects by layer and cannot tell your helper |
| 177 | +geometry from your data. And the coordinates are always in the **document's units**. |
| 178 | + |
| 179 | +## 4. Publish |
| 180 | + |
| 181 | +``` |
| 182 | +POST /publish { "project": "<projectId>", "document": "<doc>", "name": "rhino-columns.frag" } |
| 183 | +``` |
| 184 | + |
| 185 | +```json |
| 186 | +{ "ok": true, "file": "rhino-columns.frag", "items": 8, "withGeometry": 4, "bytes": 10529 } |
| 187 | +``` |
| 188 | + |
| 189 | +**Check that number.** Eight items is four points and four boxes. The first version of this uploaded |
| 190 | +915 items and 3.4 MB, because the 907 reference meshes went back up with everything else — a |
| 191 | +tessellated copy of the Revit model, republished under the Rhino model's name. Objects stamped |
| 192 | +`ThatOpen:Source` are excluded now, but if you ever see the item count in the hundreds, that is what |
| 193 | +happened. |
| 194 | + |
| 195 | +Points count as items without geometry. That is correct: they are the data the converter reads, not |
| 196 | +something to draw. |
| 197 | + |
| 198 | +## 5. The converter |
| 199 | + |
| 200 | +This is the part that is **not** in either plug-in, and should not be. It reads the published |
| 201 | +`.frag`, decides what it means, and queues a proposal. |
| 202 | + |
| 203 | +The worked example is `RhinoColumnsConverter`: points on a named layer become Revit columns of a |
| 204 | +given family and type. It is written to be read and then rewritten — the next person wants walls |
| 205 | +from curves, or louvres from blocks — but four steps survive any rewrite: |
| 206 | + |
| 207 | +1. read the source model's own data, in the source's own units |
| 208 | +2. ask what this converter already made, which comes from the queue and nowhere else |
| 209 | +3. decide: new ones are added, known ones updated, vanished ones removed |
| 210 | +4. queue one operation |
| 211 | + |
| 212 | +It persists nothing of its own. Run it on another machine, or as a cloud component, and it behaves |
| 213 | +identically, because everything it remembers it reads back from what it wrote to the platform. |
| 214 | + |
| 215 | +The byte contract is **[Writing a flow plugin](https://raw.githubusercontent.com/ThatOpen/platform_services/main/docs/flow-plugin-guide.md)**, and the reasoning |
| 216 | +behind the shape is **[the architecture notes](https://raw.githubusercontent.com/ThatOpen/platform_services/main/docs/flow-architecture.md)**. |
| 217 | + |
| 218 | +## 6. Accept it in Revit |
| 219 | + |
| 220 | +The Revit add-in's local API, `%APPDATA%\ThatOpen\revit-addin.json`, header `X-RevitFlow-Token`. |
| 221 | + |
| 222 | +**List what is waiting:** |
| 223 | +``` |
| 224 | +POST /ops |
| 225 | +``` |
| 226 | + |
| 227 | +**Check one before offering it:** |
| 228 | +``` |
| 229 | +POST /op-check { "opId": "op-…" } |
| 230 | +``` |
| 231 | + |
| 232 | +It answers with the version the proposal was authored against and the version the model is on now. |
| 233 | +A proposal does not go stale — it says "move this BY two feet", not "put it at this coordinate" — but |
| 234 | +show the user both numbers and let them decide. |
| 235 | + |
| 236 | +**Apply it, once they have said yes:** |
| 237 | +``` |
| 238 | +POST /apply-ops { "opId": "op-…" } |
| 239 | +``` |
| 240 | + |
| 241 | +Everything in the proposal happens in **one** Revit transaction: one undo, one entry in the history. |
| 242 | +There is also a **Proposals** button on the ribbon, which shows the queue and lets a person accept or |
| 243 | +refuse. Prefer it when there is somebody at the machine — accepting somebody else's change is a |
| 244 | +decision, and a decision wants the thing being decided in front of you. |
| 245 | + |
| 246 | +The change is in their local only. They still sync. |
| 247 | + |
| 248 | +## 7. Sync, and look at it |
| 249 | + |
| 250 | +Revit's own Synchronize with Central. The add-in rides it: the work reaches the team and the commit |
| 251 | +lands in the history, with the new columns in it, viewable in the app like any other change. |
| 252 | + |
| 253 | +## Traps, all of them paid for |
| 254 | + |
| 255 | +- **Rhino must be closed to install or update the plug-in.** It holds its own files, and a partial |
| 256 | + copy leaves a plug-in that is neither version. |
| 257 | +- **`script` deadlocks on any command that asks a question.** Use the endpoints. |
| 258 | +- **Units are the document's**, everywhere. Ask `POST /status` rather than assuming metres. |
| 259 | +- **The converter reads points, not solids.** Somebody modelling a beautiful steel section will |
| 260 | + convert nothing at all, with no error to explain it. |
| 261 | +- **Do not publish what you imported.** If the item count is in the hundreds, reference geometry is |
| 262 | + going back up. |
| 263 | +- **Group before concluding anything about the model.** The first rows of a listing are a sample, |
| 264 | + and on this project the sample was the exception twice running. |
0 commit comments