@thatopen/services 0.6.0 → 0.8.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/README.md +62 -62
- package/dist/built-in/index.d.ts +485 -100
- package/dist/cli/lib/revit-addin.d.ts +8 -0
- package/dist/cli.js +135 -24
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +41 -50
- package/dist/vite-env.d.ts +1 -1
- package/docs/ai-quickstart.md +138 -4
- package/docs/cli/create.md +29 -29
- package/docs/cli/local-server.md +38 -38
- package/docs/cli/login.md +32 -32
- package/docs/cli/publish.md +31 -31
- package/docs/cli/run.md +26 -26
- package/docs/cli/serve.md +43 -43
- package/docs/cli/swap.md +30 -30
- package/docs/flow-architecture.md +1 -1
- package/docs/flow-plugin-guide.md +27 -1
- package/docs/platform-token.md +81 -0
- package/docs/revit-collab-quickstart.md +115 -35
- package/docs/rhino-interop-quickstart.md +458 -0
- package/package.json +1 -1
- package/resources/AGENTS.md +1 -1
- package/src/built-in/CDEManager/example.ts +214 -214
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
- package/src/built-in/UIManager/src/app/example.ts +97 -97
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
- package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -150
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
- package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -183
- package/src/built-in/UIManager/src/git-history-panel/example.ts +70 -70
- package/src/built-in/UIManager/src/models-list/example.ts +122 -122
- package/src/built-in/UIManager/src/topics-panel/example.ts +170 -170
- package/src/built-in/index.ts +485 -100
- package/src/cli/templates/app/index.html +35 -35
- package/src/cli/templates/app/package.json +32 -32
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
- package/src/cli/templates/app/src/bim-components/index.ts +1 -1
- package/src/cli/templates/app/src/globals.ts +1 -1
- package/src/cli/templates/app/src/main.history.ts +191 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
- package/src/cli/templates/app/src/ui-components/index.ts +2 -2
- package/src/cli/templates/app/tsconfig.json +16 -16
- package/src/cli/templates/app/vite.config.js +57 -57
- package/src/cli/templates/cloud-component/README.md +205 -205
- package/src/cli/templates/cloud-component/_thatopen +5 -5
- package/src/cli/templates/cloud-component/declarations.json +4 -4
- package/src/cli/templates/cloud-component/package.json +21 -21
- package/src/cli/templates/cloud-component/src/main.ts +70 -70
- package/src/cli/templates/cloud-component/tsconfig.json +16 -16
- package/src/cli/templates/cloud-component/vite.config.js +46 -46
- package/src/cli/templates/shared/AGENTS.md +8 -8
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/src/cli/templates/shared/_gitignore +5 -5
|
@@ -0,0 +1,458 @@
|
|
|
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
|
+
Everything below is what a run of this cost to find out. None of it is guessable, and all of it
|
|
219
|
+
fails quietly when it is wrong.
|
|
220
|
+
|
|
221
|
+
### What Rhino actually publishes
|
|
222
|
+
|
|
223
|
+
One item per Rhino object, category `IFCBUILDINGELEMENTPROXY`, carrying these attributes:
|
|
224
|
+
|
|
225
|
+
| attribute | example | note |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| `Layer` | `flow:columns` | |
|
|
228
|
+
| `Type` | `Point` | `Point`, `Extrusion`, `Curve`, … |
|
|
229
|
+
| `Name` | | the object's name, usually empty |
|
|
230
|
+
| `Position` | `"-5544.4,-1413,0"` | a **string**, in the document's units |
|
|
231
|
+
| `Rhino:Definition` | | block definition, when there is one |
|
|
232
|
+
|
|
233
|
+
**The layers are published as items too**, category `IFCBUILDINGSTOREY`, geometry-less, carrying a
|
|
234
|
+
single `Name` attribute holding the layer's full path. They have no `Layer` and no `Position`. So a
|
|
235
|
+
converter that matches the layer name loosely, or that assumes every item it sees has a position,
|
|
236
|
+
reads a layer as a column. Test both fields, positively:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
if (attrs.Layer !== "flow:columns") continue;
|
|
240
|
+
if (attrs.Type !== "Point") continue; // and this is what separates points from everything else on it
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`Type` is Rhino's own geometry class name: `Point` for a point, `Extrusion` for a `_-Box`, `Curve`
|
|
244
|
+
for a line. It is the only thing distinguishing the data from the helper geometry if somebody ends up
|
|
245
|
+
putting both on one layer.
|
|
246
|
+
|
|
247
|
+
Reading it in the app, where the client is already authenticated:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const res = await client.downloadFile(fileId);
|
|
251
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
252
|
+
const model = FRAGS.EditUtils.getModelFromBuffer(bytes, false);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`getModelFromBuffer(bytes, raw)`: `raw` is false for anything downloaded, because what the platform
|
|
256
|
+
stores is compressed. Attribute rows come back as JSON triples, `[name, value, type]`, one per call
|
|
257
|
+
to `row.data(j)`.
|
|
258
|
+
|
|
259
|
+
### Units, and why the envelope has no units field
|
|
260
|
+
|
|
261
|
+
**Print both files' metadata BEFORE you write a line of the converter.** This is an ordering
|
|
262
|
+
instruction, not advice. The whole conversion rests on what those two files declare, and it takes
|
|
263
|
+
thirty seconds to find out:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
for (const id of [rhinoFileId, revitFileId]) {
|
|
267
|
+
const res = await client.downloadFile(id);
|
|
268
|
+
const m = FRAGS.EditUtils.getModelFromBuffer(new Uint8Array(await res.arrayBuffer()), false);
|
|
269
|
+
console.log(id, m.metadata());
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Measured on a live demo, 2026-08-05: a converter was written the other way round, four hundred lines
|
|
274
|
+
first and the check afterwards, and the target's metadata turned out to be `{}`. Everything above the
|
|
275
|
+
units gate was correct and none of it could run. Read the two files, then write the converter around
|
|
276
|
+
what is actually in them.
|
|
277
|
+
|
|
278
|
+
A proposal's numbers are in the terms of **what the target published**. When Revit's export declares
|
|
279
|
+
anything, it says so itself:
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
"data": { "units": "ft", "axes": "z-up", "note": "Revit's own terms, untouched. Operations are written in these." }
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Rhino publishes its document units in the same place, `model.metadata()`, under `data.units`. On the
|
|
286
|
+
sample project that is `Millimeters`. So the converter divides by **304.8**, and it is the converter's
|
|
287
|
+
job precisely because it is the only piece that knows both sides.
|
|
288
|
+
|
|
289
|
+
**When the target declares nothing.** Models published by add-in versions before 1.2.27 carry empty
|
|
290
|
+
metadata on their first commit, so `{}` is a state you will meet on any project created before then.
|
|
291
|
+
Revit's internal length unit is always decimal feet, so the factor is not actually in doubt; what is
|
|
292
|
+
missing is the file saying so. Take feet, say out loud that you are taking it, and then **prove it
|
|
293
|
+
from the data** rather than leaving it as an assumption:
|
|
294
|
+
|
|
295
|
+
- Read one existing column's `Position` out of the target, and compare it against the same column
|
|
296
|
+
measured in the source. On the sample project: `-30.19028688187479` ft times 304.8 is **-9202 mm**,
|
|
297
|
+
which is exactly the bay edge measured in Rhino.
|
|
298
|
+
- Keep a sanity check on every converted point: refuse anything landing more than about ten metres
|
|
299
|
+
from an existing element of the same category. That is what catches a factor-of-1000 error, and it
|
|
300
|
+
works whether or not the file declared anything.
|
|
301
|
+
|
|
302
|
+
**What you must never do is guess silently.** A wrong factor puts the columns a thousand times too
|
|
303
|
+
far away, and nothing about the result says why. Refusing is a worse demo and a better outcome.
|
|
304
|
+
|
|
305
|
+
### Building the payload
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
const model = EU.getModelFromBuffer(EU.newModel({ raw: true }), true);
|
|
309
|
+
return EU.edit(model, requests, { raw: false }).model;
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`newModel` hands back uncompressed bytes, so `raw` is true on the way in and false on the way out.
|
|
313
|
+
|
|
314
|
+
**Every `CREATE_ITEM` needs its own `localId`.** Three requests sharing one id collapse into a single
|
|
315
|
+
item, with no error and no warning. The first attempt at this produced 1 item from 3 requests, which
|
|
316
|
+
would have queued a proposal for one column out of four. Count from 1, and leave `localId: 0` for the
|
|
317
|
+
`UPDATE_METADATA` request that carries the envelope.
|
|
318
|
+
|
|
319
|
+
### Writing it to the platform
|
|
320
|
+
|
|
321
|
+
Three calls, in this order: create the visible index if it does not exist, upload the payload as a
|
|
322
|
+
hidden child of it, then write the entry including the id the upload returned.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
const created = await client.createFile({
|
|
326
|
+
file, name: "revitflow_ops.json",
|
|
327
|
+
parentFolderId: folderId, // parentFolderId, NOT folderId
|
|
328
|
+
projectId, // required
|
|
329
|
+
versionTag: "1",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const { hiddenFileId } = await client.createHiddenFile(
|
|
333
|
+
new File([payload], `${opId}.frag`), opsId,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
await client.updateFile(opsId, { file, versionTag: `v-${Date.now()}` });
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Two failures paid for, both at runtime, both from code that compiled and read fine:
|
|
340
|
+
|
|
341
|
+
- **"A projectId is required to upload files."** The folder field was misnamed at the same time and
|
|
342
|
+
said nothing at all, because an unknown key is dropped in silence. Fix the `projectId` alone and
|
|
343
|
+
the queue gets created at the root of the project, where nothing looks for it.
|
|
344
|
+
- **"Duplicated entry"** on the update. The version tag was numbered by counting the entries: the
|
|
345
|
+
file is created with tag `"1"`, the first entry makes the list one long, and the update asks for
|
|
346
|
+
`"1"` again. Counting is wrong past that too, since removing an op makes the next one reuse a tag.
|
|
347
|
+
Use a timestamp. This is what the Revit add-in settled on.
|
|
348
|
+
|
|
349
|
+
Miss the id `createHiddenFile` returns and the payload is unreachable forever: hidden files have no
|
|
350
|
+
queryable name, so the visible index is the only place it can be written down. If the index write
|
|
351
|
+
fails after the upload, that payload is orphaned. Harmless, invisible, and worth knowing.
|
|
352
|
+
|
|
353
|
+
### Do not learn the format from files on the machine
|
|
354
|
+
|
|
355
|
+
`PlatformClient.fromPlatformContext()` exists in the app, not in a script, so a Node one-liner cannot
|
|
356
|
+
read back what Rhino just published. The tempting fallback is the `.frag` files lying around under
|
|
357
|
+
`%APPDATA%\ThatOpen` and in `flow-reconcile`. They are somebody's earlier session. One of them held
|
|
358
|
+
four points on a layer called `Columnas`, left over from an older run, and reading it as current
|
|
359
|
+
would have built a converter against a layer name nobody was using any more.
|
|
360
|
+
|
|
361
|
+
The tables above are here so nobody has to do that. If you still want to see real bytes, publish
|
|
362
|
+
something small and read it back inside the app.
|
|
363
|
+
|
|
364
|
+
### What "it builds" does not tell you
|
|
365
|
+
|
|
366
|
+
The dev build does not typecheck. `npm run build` said **111 modules, built clean** on a converter
|
|
367
|
+
whose `tsc --noEmit` was red in four places, and the run that produced this guide read that line as
|
|
368
|
+
proof and moved on. Run the typechecker separately, and read what it says.
|
|
369
|
+
|
|
370
|
+
Its complaint was worth having. A file that does not exist yet is read with a fallback, and the
|
|
371
|
+
fallback's shape becomes the type:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
const history = await this._readJson(id, { commits: [] }); // commits: never[]
|
|
375
|
+
const head = history.commits.at(-1); // head: never
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Every field read off `head` is then an error, which is the typechecker pointing out that the empty
|
|
379
|
+
case was never really thought about. Type the fallback, and decide what an empty history means.
|
|
380
|
+
|
|
381
|
+
Three more that only fail once somebody presses the button:
|
|
382
|
+
|
|
383
|
+
- **The published file name is a parameter, not a constant.** `/publish` takes `name`. A converter
|
|
384
|
+
that hardcodes `rhino-columns.frag` answers "publish from Rhino first" when the file is right
|
|
385
|
+
there under another name. If you cannot find it, list what the folder actually holds and say so.
|
|
386
|
+
- **A loose folder match picks silently.** Falling back from an exact name to "any folder ending in
|
|
387
|
+
this" is fine with one project and wrong the first time there are two, with nothing on screen to
|
|
388
|
+
say which one it chose.
|
|
389
|
+
- **The platform round trip is not exercised by anything above.** The two failures this cost were
|
|
390
|
+
`parentFolderId` and the version tag, and both compiled, read fine, and only spoke on the first
|
|
391
|
+
real click. Press it once yourself, on something small, before showing it to anybody.
|
|
392
|
+
|
|
393
|
+
Not clicking it in front of the user is still the right call. Queuing a proposal writes into a
|
|
394
|
+
shared project, and that is theirs to trigger. Press it in a scratch document instead.
|
|
395
|
+
|
|
396
|
+
## 6. Accept it in Revit
|
|
397
|
+
|
|
398
|
+
The Revit add-in's local API, `%APPDATA%\ThatOpen\revit-addin.json`, header `X-RevitFlow-Token`.
|
|
399
|
+
|
|
400
|
+
**List what is waiting:**
|
|
401
|
+
```
|
|
402
|
+
POST /ops
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Check one before offering it:**
|
|
406
|
+
```
|
|
407
|
+
POST /op-check { "opId": "op-…" }
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
It answers with the version the proposal was authored against and the version the model is on now.
|
|
411
|
+
A proposal does not go stale — it says "move this BY two feet", not "put it at this coordinate" — but
|
|
412
|
+
show the user both numbers and let them decide.
|
|
413
|
+
|
|
414
|
+
**Apply it, once they have said yes:**
|
|
415
|
+
```
|
|
416
|
+
POST /apply-ops { "opId": "op-…" }
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Everything in the proposal happens in **one** Revit transaction: one undo, one entry in the history.
|
|
420
|
+
There is also a **Proposals** button on the ribbon, which shows the queue and lets a person accept or
|
|
421
|
+
refuse. Prefer it when there is somebody at the machine — accepting somebody else's change is a
|
|
422
|
+
decision, and a decision wants the thing being decided in front of you.
|
|
423
|
+
|
|
424
|
+
The change is in their local only. They still sync.
|
|
425
|
+
|
|
426
|
+
## 7. Sync, and look at it
|
|
427
|
+
|
|
428
|
+
Revit's own Synchronize with Central. The add-in rides it: the work reaches the team and the commit
|
|
429
|
+
lands in the history, with the new columns in it, viewable in the app like any other change.
|
|
430
|
+
|
|
431
|
+
## Traps, all of them paid for
|
|
432
|
+
|
|
433
|
+
- **Rhino must be closed to install or update the plug-in.** It holds its own files, and a partial
|
|
434
|
+
copy leaves a plug-in that is neither version.
|
|
435
|
+
- **`script` deadlocks on any command that asks a question.** Use the endpoints.
|
|
436
|
+
- **Units are the document's**, everywhere. Ask `POST /status` rather than assuming metres.
|
|
437
|
+
- **The converter reads points, not solids.** Somebody modelling a beautiful steel section will
|
|
438
|
+
convert nothing at all, with no error to explain it.
|
|
439
|
+
- **Do not publish what you imported.** If the item count is in the hundreds, reference geometry is
|
|
440
|
+
going back up.
|
|
441
|
+
- **Group before concluding anything about the model.** The first rows of a listing are a sample,
|
|
442
|
+
and on this project the sample was the exception twice running.
|
|
443
|
+
- **Every `CREATE_ITEM` needs its own `localId`.** Sharing one collapses them into a single item,
|
|
444
|
+
silently. Four columns asked for, one column proposed.
|
|
445
|
+
- **`parentFolderId`, not `folderId`, and `projectId` alongside it.** An unknown key is dropped
|
|
446
|
+
without a word, so only half the mistake reports itself.
|
|
447
|
+
- **Version tags are timestamps, not counts.** Numbering by how many entries there are collides on
|
|
448
|
+
the very first update.
|
|
449
|
+
- **Stray `.frag` files on the machine are not the current format.** They are an earlier session.
|
|
450
|
+
- **Read the two files' metadata before writing the converter, not after.** Everything the
|
|
451
|
+
conversion rests on is declared in them, or is not, and finding out costs thirty seconds at the
|
|
452
|
+
start and four hundred lines at the end.
|
|
453
|
+
- **Build only what was asked for.** An extra button and an extra layout are minutes each, and they
|
|
454
|
+
come out of the same hour as the thing that has to work.
|
|
455
|
+
- **A clean build is not a typecheck.** Vite does not run `tsc`. "Built clean" and "correct" are two
|
|
456
|
+
different sentences.
|
|
457
|
+
- **Read these guides verbatim.** A fetch that summarises gives you the shape of the thing and none
|
|
458
|
+
of the numbers, and every number here was paid for. Use the raw URL.
|
package/package.json
CHANGED
package/resources/AGENTS.md
CHANGED
|
@@ -33,7 +33,7 @@ Once you have these, you know everything available on the platform. Only then fe
|
|
|
33
33
|
|
|
34
34
|
- **The scaffold is already a complete, working viewer** (model loading, spatial tree, properties).
|
|
35
35
|
**Run it first** (`npm run dev`) to see it work, *then* extend it — don't rebuild a viewer from scratch.
|
|
36
|
-
- **
|
|
36
|
+
- **Do the thing they asked for.** If the request already says what to build, build it. Stop and ask only when you are about to change files you did not create, when the request can be read two ways that mean different work, or when the next step is destructive.
|
|
37
37
|
- If something already exists in the indexes, use it — don't reimplement it.
|
|
38
38
|
|
|
39
39
|
## Hard rules (always apply)
|