Appearance
Performance
Every snaptrude.* call is a round-trip to the host — the worker sends the call, the host executes it, and the worker awaits the result. There is no in-worker compute: math, geometry, reads, and writes are all host calls. This one fact drives every performance decision below.
Bulk over loops
A per-item loop over a single-item API is the #1 cause of slow plugins. N calls = N round-trips. Each call also consumes one token from the plugin rate limit, so a large loop is not just slow — it can fail partway with a RATE_LIMITED error (Error Handling). Whenever a plural / array API exists, do the whole job in one call.
The rule
Build the full input array first, then make one bulk call. Never interleave single create / update / delete calls inside a loop.
ts
// ❌ SLOW — N round-trips, one per room (and may hit the rate limit)
for (const r of rooms) {
await snaptrude.design.create.space(r.contour, r.height, r.label);
}
// ✅ FAST — one round-trip for the whole batch
const items = rooms.map((r) => ({ contour: r.contour, height: r.height, label: r.label }));
const ids = await snaptrude.design.create.spaces(items);Do all the per-item math and geometry up front while assembling the array; the bulk call is the last step.
Bulk / array APIs
Prefer the right column. delete, transform, and selection are array-only — there is no per-item variant, so always pass the whole set.
| Instead of looping… | …call once |
|---|---|
design.create.space | design.create.spaces(items[]) |
| recreating the same geometry N times | design.create.copy(components[], displacement, { count, mode }) |
design.update.space | design.update.spaces(items[]) |
| deleting entities one by one | design.delete.entities(components[]) |
| moving / rotating / aligning / mirroring one at a time | design.transform.move / rotate / align / mirror (components[], …) |
| selecting entities one at a time | design.selection.set / add / remove (components[]) |
Each of these commits as one undoable step — that atomicity is a bonus; the reason to reach for them is that the whole batch is a single round-trip.
Repeats: copy, don't re-create
To place N instances of the same geometry, do not create it N times. Bulk-create the unique seed(s) once, then call design.create.copy once with { count: N - 1 }.
ts
const { vec3 } = snaptrude.core.math;
// 1. Create the seed once (bulk if it's more than one space).
const [seed] = await snaptrude.design.create.spaces([{ contour, height: 3, label: "Unit" }]);
// 2. One copy call lays down the rest — one round-trip for all 9 repeats.
const copies = await snaptrude.design.create.copy(
[seed],
await vec3.new(6, 0, 0), // per-copy offset (displacement * i)
{ count: 9, mode: "unique" }
);Order matters: never copy before the create that mints the seed ids. If the copies need labels or metadata, apply it afterwards with a single design.update.spaces(items[]) — not one update per copy.
Stack identical storeys: duplicate, don't rebuild
When floors share the same layout, do not recreate each floor's geometry element by element. Duplicate the storey. entity.story.duplicate("up" | "down") copies the whole active storey — walls, floors, masses, columns, beams, staircases, furniture, and so on — into the adjacent level in one call, and creates the target storey if it doesn't exist yet. Copies are instanced by default (unique: false), so they stay linked to the source and later edits propagate; pass { unique: true } for independent geometry.
ts
// A 10-storey tower whose floors are all identical: build floor 1, then stack it.
// One call copies every element on the active storey up a level (target storey auto-created).
for (let i = 0; i < 9; i++) {
await snaptrude.entity.story.duplicate("up");
await snaptrude.entity.story.setActive(i + 2); // make the new floor the source for the next copy
}For a fixed set of captured component ids (rather than the whole active storey), stack them in a single call with design.create.copy: design.create.copy(storeyIds, oneFloorRise, { count: N - 1 }), where oneFloorRise is a Vec3 of (0, floorHeight, 0). Either way you avoid re-running the per-element creation on every floor.
Delete-and-recreate: build first, delete last
When you must replace geometry, build the replacement (bulk create) first; only after it succeeds issue one design.delete.entities(ids) for the old ids. Never delete before the replacement exists — a failed create must leave the original untouched.
When there is no bulk variant
Some creators are single-item only and have no plural sibling:
- BIM creators:
mass,slab,floor,roof,ceiling,column,beam,furniture,door,window,staircase. - Every
core.geom.create.*builder (line,arc,circle,profile,contour) produces one object per call.
A loop is unavoidable there. Keep it tight:
- Hoist constants, shared handles, and unit conversions out of the loop — compute them once, not once per iteration.
- Do no redundant reads inside the loop. Read what you need once, up front.
- If you are building N footprints only to feed N single-item creators, that footprint work is itself N round-trips — there is no way around it today, so minimise everything else.
Reads are round-trips too
design.query.* and core.geom.query.* reads cost a round-trip each. Prefer the list/batch reads (listSpaces, getBoundingBox(handles[]), …) that return many entities in one call over querying entities one at a time in a loop, and cache values you will reuse instead of re-reading them.
Checklist
- [ ] No
design.create.space/update.spaceinside a loop — use thespacesbulk variant. - [ ] Repeats use one
design.create.copywithcount, not N creates. - [ ] Deletes and transforms pass the whole set in one array call.
- [ ] Delete-and-recreate builds the replacement before deleting the old ids.
- [ ] Unavoidable single-item loops hoist constants/handles/conversions out.
- [ ] Reads are batched; reused values are cached, not re-queried.