Skip to content

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.spacedesign.create.spaces(items[])
recreating the same geometry N timesdesign.create.copy(components[], displacement, { count, mode })
design.update.spacedesign.update.spaces(items[])
deleting entities one by onedesign.delete.entities(components[])
moving / rotating / aligning / mirroring one at a timedesign.transform.move / rotate / align / mirror (components[], …)
selecting entities one at a timedesign.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.space inside a loop — use the spaces bulk variant.
  • [ ] Repeats use one design.create.copy with count, 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.