@vention/vention-skills 0.1.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.
Files changed (35) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +15 -0
  3. package/README.md +61 -0
  4. package/mcp.json +9 -0
  5. package/package.json +19 -0
  6. package/plugin.json +12 -0
  7. package/skills/vention-design/SKILL.md +522 -0
  8. package/skills/vention-machine-logic/SKILL.md +256 -0
  9. package/skills/vention-machine-logic/examples/conveyor-with-sensor/main.py +58 -0
  10. package/skills/vention-machine-logic/examples/homing-and-indexing/main.py +51 -0
  11. package/skills/vention-machine-logic/examples/parallel-conveyor-with-recipe/main.py +118 -0
  12. package/skills/vention-machine-logic/examples/parallel-conveyor-with-recipe/requirements.txt +1 -0
  13. package/skills/vention-machine-logic/examples/pick-and-place-state-machine/main.py +159 -0
  14. package/skills/vention-machine-logic/examples/pick-and-place-state-machine/requirements.txt +2 -0
  15. package/skills/vention-machine-logic/scripts/library-readme.py +233 -0
  16. package/skills/vention-machine-logic/scripts/vention-docs.py +176 -0
  17. package/skills/vention-machine-logic-hmi/SKILL.md +210 -0
  18. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/buf.gen.yaml +5 -0
  19. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/index.html +11 -0
  20. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/package.json +34 -0
  21. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/app.tsx +152 -0
  22. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/client.ts +17 -0
  23. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/main.tsx +16 -0
  24. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/logs-page.tsx +52 -0
  25. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/recipes-page.tsx +139 -0
  26. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/pages/run-page.tsx +65 -0
  27. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/station.ts +34 -0
  28. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/src/use-stream.ts +54 -0
  29. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/customui/tsconfig.json +24 -0
  30. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/models.py +13 -0
  31. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/project.json +6 -0
  32. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/proto/app.proto +192 -0
  33. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/requirements.txt +5 -0
  34. package/skills/vention-machine-logic-hmi/examples/indexing-station-hmi/server.py +242 -0
  35. package/skills/vention-monitoring/SKILL.md +82 -0
@@ -0,0 +1,522 @@
1
+ ---
2
+ name: vention-design
3
+ description: Create, build and edit Vention machine designs and design graphs for agentic layout. Use when the user wants to start a new design, lay out an automation cell, add or reposition components in a Vention design, generate a machine layout from a description, add or place a floor plan image under a design, list a design's versions, or inspect an existing design graph. Trigger on MachineBuilder designs, design graphs, assembly graphs, extrusions, actuators, robot mounting, cell footprints, floor plans, or a machine-builder URL.
4
+ license: MIT
5
+ metadata:
6
+ version: "0.1.0"
7
+ author: Vention
8
+ ---
9
+
10
+ # Vention Design Graphs
11
+
12
+ Build and modify Vention designs through the Vention MCP server.
13
+
14
+ ## Before you start
15
+
16
+ Confirm the Vention MCP server is connected by calling `whoami`. If it fails, tell the
17
+ user to connect it and stop.
18
+
19
+ Everywhere below, `design` accepts either a numeric design id or a MachineBuilder URL
20
+ (`https://vention.com/machine-builder/<id>`). A version segment in a URL is ignored by
21
+ the save tool; `parent_version` is the only version input. Pass version and id values as
22
+ strings (`parent_version: "0"`, `floor_plan_id: "401"`) — that is the verified form.
23
+
24
+ The available design tools are:
25
+
26
+ | Tool | What it does |
27
+ | --- | --- |
28
+ | `design_categories_list()` | The category codes `design_create` accepts |
29
+ | `design_create(name, category_code, ...)` | Create a new, empty design; returns `id`, `name`, `url` |
30
+ | `design_versions_list(design)` | Versions newest-first with `num_parts`, `md5`, and the version's floor plan — no graph download |
31
+ | `design_graph_download_url(design, graph_version?)` | Presigned GET for a version's graph; omit `graph_version` for current |
32
+ | `design_graph_upload_create(design)` | Presigned POST for uploading a new graph |
33
+ | `design_version_save(design, upload_key, parent_version, floor_plan_id?)` | Commit an uploaded graph as a new version |
34
+ | `design_floor_plan_upload_create(design, content_type)` | Presigned POST for a floor plan image |
35
+ | `design_floor_plan_create(design, metadata, file_upload_key\|image_id)` | Attach the uploaded image to the design as a floor plan |
36
+
37
+ The save and floor-plan tools are marked **EXPERIMENTAL — provided without support**,
38
+ and they change real designs. State plainly what a write will change before you make it.
39
+ Get the user's explicit go-ahead before the first write into any design you did not
40
+ create in this session; after that, keep narrating each write without re-asking.
41
+
42
+ There is no tool to list the user's designs, rename or delete one, or delete a floor
43
+ plan. If you need an existing design, ask for its id or URL — never guess one.
44
+
45
+ ## Creating a design
46
+
47
+ `design_create(name, category_code, description?, tags?, visibility?)` makes a new,
48
+ empty design in the user's account and returns its `id`, `name` and `url`.
49
+
50
+ - `category_code` is required and must come from `design_categories_list`. Codes come in
51
+ two shapes: two letters for a top-level category (`MF` Manufacturing, `AS` Assembly,
52
+ `MH` Material Handling, `PZ` Packaging, `ME` Other Equipment), and two-plus-two for a
53
+ subcategory (`MF-RC` Machine Tending, `PZ-CB` Cobot Palletizer, `MH-CV` Conveyors, and
54
+ so on). Those shapes are just the pattern — the codes above are real, but the list is
55
+ longer and can change. **Never guess a code.** Call `design_categories_list` and match
56
+ the user's words; if several plausibly match, or the user named no category, ask.
57
+ - `visibility` is `TEAM` (the default), `ORGANIZATION` or `PRIVATE`. The default shares
58
+ the design with the user's team — if that's not obviously what they want, ask before
59
+ creating rather than after.
60
+ - A created design starts with **no graph and no versions at all**:
61
+ `design_versions_list` returns `[]` and `design_graph_download_url` errors with
62
+ `NotFound: Design has no assembly graph`. Both are expected, not failures.
63
+
64
+ ### Saving the first version
65
+
66
+ There is no parent version to start from, so pass **`parent_version: "0"`** on the
67
+ first save into a freshly created design. That is verified to work and produces version
68
+ `1`. You must supply a complete graph yourself, including the `config` vertex — an
69
+ API-created design does not come with one (see the note under the schema below).
70
+
71
+ ## Understanding the design graph
72
+
73
+ A Vention design is stored as an assembly graph: a set of parts (extrusions, plates,
74
+ actuators, robots, end effectors, loose hardware) plus the mating relationships
75
+ ("edges") that position them relative to one another. Editing a design means
76
+ transforming that graph, not editing a 3D scene directly. There is no concept of an
77
+ instanced/referenced sub-assembly in the graph — everything is flattened to individual
78
+ parts, every time.
79
+
80
+ Graphs are multi-megabyte files. They move by reference, never inline.
81
+ **Never read a graph into the conversation — download it and process it with code.**
82
+
83
+ ### Schema (as observed; may evolve — treat as a map, not a spec)
84
+
85
+ Field names and shapes below come from reading real graphs, and the app may add or
86
+ change keys. What is dependable is the *structure* — vertices with ids, edges
87
+ referencing vertex ids, the `nextVID`/`nextEID` counters, and the column-major
88
+ `local_matrix`. Preserve keys you don't understand rather than dropping them.
89
+
90
+ Top level:
91
+ ```
92
+ {
93
+ "format_version": <int>, // schema version; can differ design-to-design
94
+ "format_version_initial": <int>,
95
+ "label": <string>,
96
+ "attributes": {
97
+ "design_id": <int>,
98
+ "design_version": null,
99
+ "design_bbox": {"min": {x,y,z}, "max": {x,y,z}},
100
+ "cloned_from": {"design_id": ..., "graph_version": ...}, // optional
101
+ "measurements": {"type": "face", "visible": true},
102
+ "feedback": {...}, "cut_to_length": {...} // optional
103
+ },
104
+ "vertices_by_index": [ <vertex>, ... ],
105
+ "edges": { "by_id": { "<edge_id>": <edge>, ... } },
106
+ "nextVID": <int>, // next free vertex id — must be > every vertex id used
107
+ "nextEID": <int> // next free edge id — must be > every edge id used
108
+ }
109
+ ```
110
+
111
+ Vertex (one part/node):
112
+ ```
113
+ {
114
+ "id": <int>,
115
+ "attributes": {
116
+ "part_number": <string>, // absent on the "config" vertex
117
+ "short_name": <string>,
118
+ "vertex_type": "part" | "imported_part" | "addon" | "panel" | "config",
119
+ "local_matrix": {"m": [ ...16 floats... ]}, // absent on some addon parts
120
+ "opacity": <number>, "visibility": <bool>, "dofs": [...], "hoops_id": <int>
121
+ },
122
+ "edges": [ <edge_id>, ... ] // ids of every edge incident on this vertex
123
+ }
124
+ ```
125
+ - `local_matrix.m` is a **column-major 4x4** matrix flattened to 16 values; translation
126
+ is at indices 12 (x), 13 (y), 14 (z). To rigidly move a part, only touch those three.
127
+ - **Y is Vention's vertical axis — the floor is Y=0.** Not Z. (HOOPS/Three.js viewers
128
+ default to Y-up, and this matches it.) Confirmed the hard way: a merged design's
129
+ bbox looked entirely Z-negative, which read as "the whole thing is below the floor
130
+ on Z" — but the actual symptom (a design sitting sunk about halfway into the ground)
131
+ came from its Y range straddling 0. Shifting Z did nothing for the sink; shifting Y
132
+ (`m[13]`) so the bbox's min Y lands at 0 fixed it. **Don't infer the vertical axis
133
+ from bbox magnitude — check Y first.**
134
+
135
+ ### Coordinate and part reference
136
+
137
+ An extrusion's own local space runs its length along local Z (0 to L), with a
138
+ 45x45mm cross-section (local X/Y ∈ [-22.5, 22.5]) for the common `ST-EXT-001-XXXX`
139
+ line — `XXXX` is the length in mm and must be a multiple of 45. Never invent a length
140
+ that isn't.
141
+
142
+ Connector strings follow `cma_TYPE|PART_NUMBER|GENDER|FACE_ID`. On extrusions:
143
+ `ca1`-`ca4`/`cb1`-`cb4` are the male end faces (near/far), `ba1`-`ba4`/`bb1`-`bb4` are
144
+ the female T-slots along the length (near/far). `edges.by_id[...].attributes.distance1`
145
+ is the position in mm along the female slot. Gussets (`ST-GP-003-0001`) expose male
146
+ faces `a1`/`a2` (front) and `ab1`/`ab2` (back of the same arms) — using the wrong pair
147
+ flips the gusset's orientation.
148
+
149
+ This is enough to sanity-check a graph you're editing. For full worked recipes (a
150
+ table-frame leg/rail/gusset layout with exact corner-by-corner rotations, and straight/
151
+ curved conveyor assemblies with their full BOMs and connector maps), see
152
+ `vention-ai-design-skill.md` at the project root — it drives the same graph through
153
+ live browser manipulation (Playwright + webpack-extracted `loadScene`) rather than the
154
+ download/upload/save API this skill uses, but the graph schema, coordinate convention,
155
+ and connector system it documents are the same ones described above.
156
+
157
+ Two more notes on vertices:
158
+
159
+ - `vertex_type: "addon"` parts (cable ties, loose hardware counted for BOM only) often
160
+ have no `local_matrix` and no edges. Don't try to position them.
161
+ - A design that has ever been saved carries one `vertex_type: "config"` vertex holding a
162
+ `configModels` root object. It is not a physical part. **Preserve it as-is** when
163
+ merging content into a design; don't delete or overwrite it. A design created through
164
+ `design_create`, by contrast, has *no graph at all* until your first save — so when
165
+ you author that first graph, include a config vertex yourself:
166
+ `{"id": <vid>, "attributes": {"vertex_type": "config", "configModels": {}}, "edges": []}`.
167
+ Note that `num_parts` in save results and `design_versions_list` counts this vertex,
168
+ so a graph with one real part reports `num_parts: 2` (verified). Don't read `num_parts`
169
+ as a BOM count.
170
+
171
+ Edge (one mating/joint relationship):
172
+ ```
173
+ "<edge_id>": {
174
+ "id": <int>,
175
+ "attributes": {
176
+ "connector1": <string>, "connector2": <string>, // "cma_1|<part_number>|male/female|<port>"
177
+ "user_specified": <bool>, "distance1": <number>, "edge_type": "connection"
178
+ },
179
+ "vertices": [ <vertex_id_1>, <vertex_id_2> ]
180
+ }
181
+ ```
182
+ Connector/distance attributes are relative between the two parts, not world-absolute —
183
+ they stay valid under a rigid translation of the parts involved.
184
+
185
+ IDs (`vertices_by_index[].id`, `edges.by_id` keys) are plain counters, unique within a
186
+ single graph and carrying no meaning across designs. The one invariant that matters is
187
+ that `nextVID`/`nextEID` sit strictly above every id in use; `max(id) + 1` is the
188
+ obvious way to satisfy it. Vertex ids and edge ids count independently.
189
+ Real graphs are not guaranteed to start counting at 1 — vertex ids beginning at
190
+ `4294967296` (2^32) and edge ids at `8589934593` (2^33 + 1) are common. Read the
191
+ existing counters rather than assuming a range.
192
+
193
+ ## Reading a design
194
+
195
+ Start with `design_versions_list(design)` — it is cheap, downloads nothing, and answers
196
+ most questions on its own. Per version, newest first: `version`, `version_name`,
197
+ `num_parts`, `size_bytes`, `md5`, `created_at`, plus that version's floor plan
198
+ (`floor_plan_url`, `floor_plan_file_name`, `floor_plan_metadata`; all `null` when the
199
+ version has none). Use it to see whether a design has content, how big it is, how many
200
+ parts it holds, and which version you should download — before spending a download.
201
+
202
+ Two quirks: `size_bytes` and `md5` can come back `null` for a version saved seconds
203
+ ago and fill in shortly after, and `floor_plan_url` expires — call the tool again for a
204
+ fresh link rather than reusing an old one.
205
+
206
+ Then, only if you need the geometry:
207
+
208
+ 1. `design_graph_download_url(design)` — omit `graph_version` for current. The result
209
+ gives you `graph_version` — keep it, you'll need it as `parent_version` if you save.
210
+ 2. Download the gzip file with a shell command (the tool gives you the exact `curl`)
211
+ and parse it in code. If it errors `NotFound: Design has no assembly graph`, the
212
+ design has never had content saved — treat it as blank, but still download it once
213
+ more right before you save (see below) since that can change.
214
+
215
+ ## Writing a design
216
+
217
+ 1. Transform the graph in code, writing the result to a local file. If you're adding
218
+ content to an existing design (rather than working from scratch), start from its
219
+ *current* downloaded graph and append to it — keep its `config` vertex and any
220
+ existing content, and continue its `nextVID`/`nextEID` counters rather than
221
+ restarting them.
222
+ 2. **Validate before uploading** — cheap and catches most mistakes:
223
+ - every vertex id and every edge id is unique
224
+ - every edge's `vertices` entries exist in `vertices_by_index`
225
+ - every vertex's `edges` entries exist in `edges.by_id`
226
+ - `nextVID` > every vertex id used, `nextEID` > every edge id used
227
+ 3. **Settle `parent_version` before you upload.** Check the design's current version with
228
+ `design_versions_list` (or `design_graph_download_url`) — don't reuse a number from
229
+ earlier in your work, since a design can advance versions with no content change (see
230
+ gotcha below). If it has moved past the version you transformed in step 1, go back to
231
+ step 1 and redo the edit on the current graph; the work you already did is stale.
232
+ For the first save into a design created by `design_create`, there is no current
233
+ version — use `parent_version: "0"`.
234
+ 4. `design_graph_upload_create(design)` — mint an upload. Gzip your file, then **POST**
235
+ it (multipart form) to `post_url` using the returned `form_fields` verbatim, with
236
+ the file field last. This is not a plain PUT. A `204` with an empty body is success;
237
+ there is no other confirmation, and the upload changes nothing on its own.
238
+ 5. `design_version_save(design, upload_key, parent_version, floor_plan_id?)` — commit as
239
+ a new version. Rails assigns the number; never pick one yourself. Pass
240
+ `floor_plan_id` if this version should keep a floor plan (see Floor plans below).
241
+ If it's rejected as stale, the error names the actual current version; download that
242
+ version, reapply your change on top of it, **re-upload** (see the next gotcha), and
243
+ retry.
244
+
245
+ Saves **append a new immutable version**. They never overwrite the version you started
246
+ from, so the user can always roll back. Saves are only accepted on top of the current
247
+ version — editing an older version is not supported.
248
+
249
+ ### The app normalizes what you save
250
+
251
+ A minimal hand-authored graph is accepted and then tidied up the next time the design is
252
+ opened and saved in MachineBuilder. Observed across one such round trip:
253
+
254
+ - `configModels: {}` on the config vertex was replaced with a real root object
255
+ (a uuid keyed to `{"id", "type": "root", "properties": {"version": 37}, "parentId":
256
+ null, "childrenIds": []}`). So an empty object is a valid thing to author — the app
257
+ fills it in.
258
+ - `opacity: 1` and `visibility: true` were added to the part vertex.
259
+ - `format_version` was rewritten `28 → 29`, with `format_version_initial` left at `28`.
260
+ Don't hardcode a `format_version`; carry over whatever the design currently reports,
261
+ and expect the app to raise it.
262
+ - `design_bbox` was reset to all zeros, discarding the extents that were written into
263
+ it. Treat the bbox as app-maintained: read it for sizing decisions if it looks
264
+ populated, but don't count on a value you write surviving, and don't treat zeros as
265
+ meaning the design is empty.
266
+
267
+ Geometry itself came back untouched — part number, `short_name` and `local_matrix`
268
+ survived exactly. The lesson is to author the minimum honestly and let the app enrich
269
+ it, rather than trying to synthesize fields you haven't observed.
270
+
271
+ ### Gotcha: a rejected save destroys your upload
272
+
273
+ A stale-`parent_version` rejection consumes the uploaded file anyway. Retrying the save
274
+ with a corrected `parent_version` and the same `upload_key` fails with
275
+ `NotFound: no uploaded file at this key`. Every save attempt — including a retry after
276
+ a rejection — needs a fresh `design_graph_upload_create` and a fresh POST. Verified.
277
+
278
+ ### Gotcha: a live browser session can fork the version history
279
+
280
+ If the user has the design open in MachineBuilder while you save via the API, its tab
281
+ keeps auto-saving on its own timeline — even with zero content changes (observed: v3 →
282
+ v5 with an identical empty graph). Your API save branches off whatever version was
283
+ current *at that moment* (e.g. v5 → v6), but the live tab has no idea your v6 exists
284
+ and just keeps auto-saving its own sibling branch (e.g. v5 → v5.1). The user opening
285
+ the design in their browser will see the live branch, not yours, even though yours has
286
+ your changes.
287
+
288
+ Fix: tell the user to open the specific version number you saved in MachineBuilder and
289
+ save from there — that makes the editor adopt it as the branch it continues on. Warn
290
+ them about this explicitly whenever you save into a design you know is open live.
291
+
292
+ ## Floor plans
293
+
294
+ A floor plan is a plan-view image (JPEG, PNG or BMP) attached to a design so a layout
295
+ can be built on top of the real building. It lives entirely on the version pin —
296
+ **a downloaded graph contains no reference to the floor plan at all** (verified by
297
+ diffing a graph before and after a floor plan was added), so `floor_plan_id` on the save
298
+ is the whole mechanism. Nothing about it needs to go into the graph JSON.
299
+
300
+ Adding one takes **four** steps — three to create it, then a save to pin it. Stopping
301
+ after step 3 attaches the floor plan but displays it nowhere; that is the step most
302
+ easily forgotten:
303
+
304
+ 1. `design_floor_plan_upload_create(design, content_type)` — mint a presigned POST.
305
+ `content_type` must be one of `image/jpeg`, `image/png`, `image/bmp`, and must be
306
+ sent back as the `Content-Type` form field (unlike the graph upload, the floor-plan
307
+ form fields include it). **The cap is 5 MB** (`max_bytes: 5242880`) — much tighter
308
+ than the graph upload's 100 MB. Check the file size before minting; if it's over,
309
+ downscale the image rather than retrying.
310
+ 2. POST the image multipart to `post_url` with every `form_fields` pair verbatim and
311
+ the file part last. A 204 is success.
312
+ 3. `design_floor_plan_create(design, metadata, file_upload_key: <key>)` — turns the
313
+ upload into a floor plan on the design. Returns `floor_plan_id`, plus `image_url` and
314
+ `image_file_name`. **Record the `floor_plan_id` and the image id from `image_url` in
315
+ your reply to the user** — see the recovery note below for why. (`image_file_name`
316
+ will be the upload key's UUID, not the user's filename, since the API mints the key;
317
+ a UI upload keeps the original name. Nothing depends on it.)
318
+ 4. `design_version_save(design, upload_key, parent_version, floor_plan_id: "<id>")` —
319
+ pin it. Until a version pins it, the floor plan is displayed nowhere.
320
+
321
+ Step 4 is a normal graph save, so it needs a graph: follow "Writing a design" above. To
322
+ add a floor plan without changing geometry, download the current graph and re-upload it
323
+ unmodified with the `floor_plan_id` set.
324
+
325
+ ### Pinning is per-version, and silently drops
326
+
327
+ `floor_plan_id` is not sticky. **Omit it on a later save and the new current version has
328
+ no floor plan** — verified: v1 saved with `floor_plan_id` had the plan, v2 saved without
329
+ it came back with `floor_plan_url: null`, and the design showed no floor plan even
330
+ though nothing was deleted. Nothing warns you.
331
+
332
+ So before any save into a design that has a floor plan, check the current version's
333
+ floor plan with `design_versions_list` and restate its `floor_plan_id` on your save.
334
+
335
+ ### Recovering a floor_plan_id you don't have
336
+
337
+ `design_versions_list` returns `floor_plan_url`, `floor_plan_file_name` and
338
+ `floor_plan_metadata` — **not `floor_plan_id`**. The only place the id appears is the
339
+ return value of `design_floor_plan_create`, so an id created in an earlier session is
340
+ not directly recoverable, and there is no tool that lists a design's floor plans.
341
+
342
+ What *is* recoverable is the **image id**, from the `floor_plan_url` path
343
+ (`.../design_floor_plan_image/file/396/…` → `396`). So to keep a floor plan whose id you
344
+ have lost: read the current version's `floor_plan_metadata` and image id from
345
+ `design_versions_list`, call `design_floor_plan_create` again with that `image_id` and
346
+ that same metadata, and pin the fresh `floor_plan_id` it returns. The image is not
347
+ re-uploaded and the placement is preserved — you just get a new record pointing at the
348
+ same image.
349
+
350
+ Because of this, always surface the `floor_plan_id` to the user when you create one, so
351
+ it is in the conversation rather than only in a tool result.
352
+
353
+ ### Reusing and repositioning an image
354
+
355
+ `design_floor_plan_create` takes **either** `file_upload_key` (a fresh upload) **or**
356
+ `image_id` — never both. `image_id` is the numeric id in the returned `image_url` path
357
+ (`.../design_floor_plan_image/file/396/...` → `image_id: "396"`), and it lets you create
358
+ a second floor plan from an image already uploaded. That is how you reposition or
359
+ rescale a plan: create a new floor plan record from the same `image_id` with different
360
+ `metadata`, then pin the new `floor_plan_id`. Verified. Don't re-upload the image to
361
+ move it.
362
+
363
+ Floor plans accumulate on the design and there is no delete tool, so don't churn out
364
+ records experimentally.
365
+
366
+ ### The metadata field
367
+
368
+ `metadata` is required, stored **verbatim**, and **not validated by the tool** — a wrong
369
+ shape is accepted happily and then simply fails to place the image. So write exactly
370
+ what MachineBuilder itself writes. This is the shape the UI produces, read back from a
371
+ UI-created floor plan (verified):
372
+
373
+ ```json
374
+ {
375
+ "version": 1,
376
+ "fileSize": 179580,
377
+ "dimensions": { "width": 578, "height": 596 },
378
+ "matrix": [12731.246649383824, 0, 0, 0,
379
+ 0, 1, 0, 0,
380
+ 0, 0, 13127.721458534186, 0,
381
+ 0, 0, 0, 1]
382
+ }
383
+ ```
384
+
385
+ - `version` — the metadata schema version. `1` at time of writing. Copy the existing
386
+ value when editing a design that already has a floor plan.
387
+ - `fileSize` — the image's size in bytes.
388
+ - `dimensions` — the image's pixel width and height.
389
+ - `matrix` — a **flat 16-float column-major 4x4**, at the top level. Note it is *not*
390
+ nested under a `transform` key and there is no `fileName` key. Don't add keys the app
391
+ doesn't write.
392
+
393
+ ### How the matrix works
394
+
395
+ It scales a unit plane onto the floor, in the graph's own Y-up convention:
396
+
397
+ - `matrix[0]` = the plan's **X extent in mm**, `matrix[10]` = its **Z extent in mm** —
398
+ both horizontal, matching Y being vertical.
399
+ - `matrix[5]` = `1`. The plane has no thickness; leave it at 1.
400
+ - `matrix[12..14]` = translation, `0, 0, 0` in the observed case. Off-diagonal terms
401
+ were all zero — no rotation or shear.
402
+
403
+ The two extents encode one uniform scale. In the observed floor plan,
404
+ `12731.2466 / 578 == 13127.7215 / 596 == 22.026378` mm per pixel, and the extents'
405
+ ratio matches the image aspect ratio exactly. So to build a matrix:
406
+
407
+ ```
408
+ s = millimetres per pixel # from the user, or from a known real-world dimension
409
+ sx = dimensions.width * s
410
+ sz = dimensions.height * s
411
+ matrix = [sx,0,0,0, 0,1,0,0, 0,0,sz,0, tx,ty,tz,1]
412
+ ```
413
+
414
+ That plan was 578 x 596 px at 22.026 mm/px — 12.73 m x 13.13 m of floor. **Ask the user
415
+ for the scale**; never assume one. The cleanest question is what a known distance on the
416
+ drawing measures in the real building, then divide by its pixel length.
417
+
418
+ > **Still unverified:** where the plane's origin sits — whether zero translation puts
419
+ > the image's corner or its centre at the world origin — and whether the app honours a
420
+ > non-zero translation or rotation written through the API. Only the zero-translation
421
+ > case has been observed. When you write a floor plan, say the placement needs a look,
422
+ > ask the user to confirm the image lands where they expect, and offer to adjust. Don't
423
+ > call a floor plan correctly placed because the API calls succeeded.
424
+
425
+ ## Duplicating / placing a copy of an existing design
426
+
427
+ There's no sub-assembly instancing in this graph format — "place a copy of design X"
428
+ means deep-copying its parts and joints, not inserting a reference:
429
+
430
+ 1. Download the source design's current graph.
431
+ 2. For each copy you want: deep-copy every vertex and every edge, assigning fresh ids
432
+ (continue counting up from the target's `nextVID`/`nextEID`), remapping the vertex
433
+ ids referenced in each edge's `vertices` and each vertex's `edges` list consistently.
434
+ 3. Rigidly offset each copy so copies don't overlap: add a delta to `m[12]`/`m[13]`/
435
+ `m[14]` (x/y/z translation) on every vertex in the copy that has a `local_matrix`.
436
+ Skip vertices without one (e.g. `addon` parts) — leave them untouched. Size the
437
+ offset using the source's `design_bbox` extent plus a margin, along **X or Z** — a
438
+ horizontal axis. Offsetting along Y (`m[13]`) would lift the copy off the floor or
439
+ sink it into the ground, since Y is the vertical axis.
440
+ Leave edge attributes (connector strings, `distance1`) unchanged — they're
441
+ part-relative and remain valid under a rigid translation.
442
+ 4. Merge the copies' vertices/edges into the target's existing `vertices_by_index` /
443
+ `edges.by_id` (keeping the target's own `config` vertex and any existing content),
444
+ recompute `design_bbox` as the union, and set `nextVID`/`nextEID` past every id used.
445
+ 5. Run the validation pass above before uploading.
446
+
447
+ If the source's `format_version` differs from the target design's, treat the merge as
448
+ unverified: the JSON can be structurally self-consistent and still render wrong if the
449
+ app expects newer-schema shapes the older source never had. Say so to the user and ask
450
+ them to visually confirm the result in MachineBuilder rather than asserting it worked.
451
+
452
+ ## Working practice
453
+
454
+ Establish the requirement before building anything. Ask for whatever is missing:
455
+
456
+ - The part or payload — dimensions, weight, orientation
457
+ - The process — pick and place, palletizing, machine tending, assembly
458
+ - The robot, if one is specified, and its reach and payload needs
459
+ - The available footprint and any fixed obstacles — if they have a plan-view drawing of
460
+ the space, offer to attach it as a floor plan and lay the cell out on it
461
+ - Throughput target
462
+
463
+ Then plan the layout before writing any graph: place the robot first, then position
464
+ peripherals inside its reach envelope, then add structure.
465
+
466
+ ## Rules
467
+
468
+ - Never invent a Vention part number. Use parts you found in an existing graph or that
469
+ the user supplied.
470
+ - Never invent a `category_code`. Read them from `design_categories_list`.
471
+ - Never invent load ratings, reach figures, or payload capacities.
472
+ - Report what you changed, in plain language, before the user opens the design.
473
+ - If a save fails validation, show the user the errors rather than retrying blindly.
474
+ - The save and floor-plan tools are experimental and act on real designs in the user's
475
+ account. Confirm before the first write into a design you didn't create this session.
476
+
477
+ ## Safety
478
+
479
+ A generated layout is an engineering starting point, not a validated machine. Never
480
+ describe one as safety-validated, compliant, or ready to build. Always tell the user to
481
+ review the result in MachineBuilder, run the Automated Design Checker, and have any
482
+ safety-relevant design reviewed by a qualified engineer before anything is built.
483
+
484
+ ## Common mistakes
485
+
486
+ - Downloading a graph into context instead of into a file. Graphs are far too large.
487
+ - Assuming a save overwrites the current version. It appends a new one.
488
+ - Assuming the vertical axis from a bbox's magnitude or sign instead of checking Y
489
+ directly. Y is the floor-level axis (Y=0); guessing Z produced a fix that moved the
490
+ design sideways and left it still sunk into the ground.
491
+ - Reusing a `parent_version` from earlier in a session instead of re-checking current
492
+ right before saving — sessions and live browser tabs can both advance it.
493
+ - Saving into a design that's open live in the user's browser without warning them
494
+ about version forking (see gotcha above), leaving them looking at the wrong branch.
495
+ - Treating a graph download error as fatal without retrying. Read the message:
496
+ `NotFound: Design has no assembly graph` means the design is genuinely blank — accept
497
+ it and move on. Anything else, including a version that was saved moments ago and
498
+ isn't downloadable yet, is worth one retry before you report a problem.
499
+ - Designing past the robot's reach envelope because the reach figure was assumed rather
500
+ than looked up.
501
+ - Treating `design_versions_list` returning `[]`, or `NotFound: Design has no assembly
502
+ graph`, as a failure. On a freshly created design both are the expected answer.
503
+ - Guessing a `parent_version` for the first save into a new design instead of using `"0"`.
504
+ - Retrying a stale-rejected save with the same `upload_key`. The file is already gone —
505
+ mint a new upload and POST again.
506
+ - Omitting `floor_plan_id` on a save into a design that has a floor plan, silently
507
+ dropping it from the new current version.
508
+ - Uploading a floor plan image and stopping there. Nothing is attached until
509
+ `design_floor_plan_create`, and nothing is visible until a save pins the id.
510
+ - Re-uploading a floor plan image just to move it, instead of creating a new floor plan
511
+ from the same `image_id` with a different transform.
512
+ - Sending a floor plan image over the 5 MB cap, or with a `content_type` other than
513
+ JPEG/PNG/BMP.
514
+ - Reporting a floor plan as correctly placed because the API calls succeeded. The
515
+ metadata is stored unvalidated; only the user looking at the design confirms placement.
516
+ - Inventing a floor-plan `metadata` shape. Write the UI's shape — flat top-level
517
+ `matrix`, `version`, `fileSize`, `dimensions` — not a plausible-looking alternative.
518
+ - Assuming a scale for a floor plan image. Ask what a known distance on the drawing
519
+ measures in the building.
520
+ - Hardcoding `format_version`, or trusting a `design_bbox` you wrote to survive a save.
521
+ - Downloading a graph to answer a question `design_versions_list` already answers
522
+ (part count, size, whether the design has content, which version to use).