Skip to content

Handles

A handle is an opaque, kind-tagged reference to a host-side object that crosses the plugin ↔ host RPC boundary by identity rather than by value. Only JS primitives (number, string, boolean) and enum strings cross the boundary by value — every structured type (a vector, a curve, a profile, a brep face, a scene entity) is represented by a handle.

At runtime an arena handle (value / resource / topology kinds) is a small Handle object wrapping its wire id — a string of the form "<kind>_<token>" (e.g. "profile_k3f9a2x"). The id is available as handle.id (and String(handle)); on the RPC wire only the id string travels. Entity-style handles (components, materials, underlays, terrain, import jobs) remain raw id strings — they are persistable and equality-matched against ids embedded in results. At compile time the phantom __handle brand makes handles of different kinds incompatible: a ProfileHandle cannot be passed where a Vec3Handle is expected.

The SDK guarantees one live Handle instance per id, so ===, Set membership, and Map keys work exactly as they did when handles were plain strings. Handles your code has completely dropped are auto-released eventually (a garbage-collection backstop) — but deterministic release via core.handles is the contract for loops and long-running plugins.

ts
// A handle is minted by the host and passed back into later calls as-is.
const v = await snaptrude.core.math.vec3.new(0, 0, 10);
// v is an opaque Handle; v.id is "vec3_a1b2c3" — never parse or construct it yourself.

Boundary rule: a Handle object only means something inside your worker. Crossing any non-RPC boundary — sendToUI to your popup, persistence, logging — send handle.id. (Structured clone strips the class; a stripped handle is accepted back by the API as a mercy rule, but .id is the contract.)

Handle kinds

Value handles (immutable)

Immutable mathematical/geometric values. Operations take handles in and return new handles out — a value handle is never mutated. All math and geometry on them runs host-side via RPC (there is no in-worker compute), so even vec3.add must be awaited.

HandleKind prefixWhat it references
Vec3Handlevec3_A 3D vector
QuatHandlequat_A quaternion (rotation)
LineHandleline_A line segment curve
ArcHandlearc_An arc curve
CurveHandleline_ or arc_A curve is a line or an arc — its handle is one of those two kinds (a union type, not a kind of its own)
CircleHandlecircle_A circle. A circle is a closed curve — it deliberately does not join CurveHandle (it has no stable start/end and would break the shared curve read contract), so it carries its own kind and its own core.geom.query.circle namespace

Resource handles (session-ephemeral)

Host-resident geometry objects owned by your plugin for the current session. They are freed via core.geom.delete and count against a per-plugin quota — release them when you are done.

HandleKind prefixWhat it references
ProfileHandleprofile_A host-resident profile
ContourHandlecontour_A host-resident contour (outer profile + hole profiles)

BREP topology handles (read-only resource family)

Session-ephemeral, identity-deduped resource handles onto the boundary-representation topology graph of a mesh. They are read-only — walked via core.geom.query.{brep, face, edge, halfedge, vertex} — and they go stale on undo/redo/re-topologize. Obtain a BrepHandle via design.query.geometry.getBrep; every other topology handle is minted by walking from a brep/face/edge/vertex handle already resolved off that brep. The registry dedupes by identity, so re-enumerating the same brep returns the same handles.

HandleKind prefixWhat it references
BrepHandlebrep_The boundary representation of a mesh
FaceHandleface_A face of a brep
EdgeHandleedge_An edge of a brep
HalfEdgeHandlehalfedge_A directed half-edge of a brep (kind token is all-lowercase halfedge)
VertexHandlevertex_A vertex of a brep

Entity handles (stable, persistable)

References to scene/project entities, resolved live against the host scene index rather than a resource registry. They have no kind prefix, no quota, and nothing to dispose. They are stable across undo/redo and can be persisted across sessions.

HandleRuntime formWhat it references
ComponentHandleRaw Component.id (no component_ prefix)A scene BIM entity (wall, space, slab, floor, …). The canonical return handle of every design.create.* creator — it round-trips with the ids the host already hands out
MaterialHandleRaw material name (Material.name)A project-scoped material, resolved live by name. Not a scene entity

Lifetime and staleness

Every arena handle (value, resource, topology) can be freed with core.handles.release, swept by kind with releaseAll, or managed with mint-recording scopes (beginScope/endScope). Releasing frees host memory and quota room; it never deletes engine geometry.

  • Value handles are immutable — they cannot become "wrong", only unknown to a new session. Every math op mints a new one: release them (or use a scope) in hot loops.
  • Resource handles (profile, contour) live until you release them or the session ends. They count against a per-plugin quota. (core.geom.delete still works but is deprecated in favor of core.handles.release.)
  • BREP topology handles are invalidated by undo/redo and by any operation that re-topologizes the mesh. After such an event, re-fetch the brep via design.query.geometry.getBrep and re-walk.
  • Entity handles (ComponentHandle, MaterialHandle) are stable across undo/redo and persistable across sessions. They have no registry entry — nothing to release. They can still fail to resolve if the underlying entity has been deleted from the scene.
  • Dropped handles (no reference anywhere in your code) are auto-released eventually by the SDK's GC backstop — a safety net, not a strategy.

HandleInvalidError

Passing an invalid, stale, or foreign handle (one minted for another plugin) rejects the call with HandleInvalidError, code HANDLE_INVALID. Client-side validation checks only the "<kind>_" prefix shape; existence, kind, and ownership are enforced host-side by the handle registry.

ts
try {
  await snaptrude.core.geom.query.face.getIndex(staleFace);
} catch (e) {
  if (e.code === "HANDLE_INVALID") {
    // face was stale — e.g. an undo re-topologized the mesh. Re-fetch the brep.
  }
}

Handles vs primitive records

Handles carry identity in; reads return plain primitive records out. Component reads never return another handle:

RecordShapeReturned by
Vec3Components{ x: number, y: number, z: number }e.g. vec3.components(v)
QuatComponents{ x: number, y: number, z: number, w: number }e.g. quat.components(q)
BBoxComponents{ min: Vec3Components, max: Vec3Components }Bounding-box reads

Curves have no by-value record: a curve crosses the boundary as a CurveHandle (line/arc), and its coordinates are read via the core.geom.query.curve API.

Do / don't

ts
// DO: treat handles as opaque tokens — mint via the API, pass back as-is.
const a = await snaptrude.core.math.vec3.new(1, 0, 0);
const b = await snaptrude.core.math.vec3.new(0, 1, 0);
const sum = await snaptrude.core.math.vec3.add(a, b); // a and b are unchanged

// DO: read values out as primitive records.
const { x, y, z } = await snaptrude.core.math.vec3.components(sum);

// DO: free handles you no longer need (per-plugin quotas) — any arena kind, batched.
await snaptrude.core.handles.release([profile, sum, a, b]);

// DO: wrap loop iterations in a scope — everything minted inside is freed at endScope.
const scope = await snaptrude.core.handles.beginScope();
// ... mint freely ...
await snaptrude.core.handles.endScope(scope);

// DO: persist entity handles — they are stable across undo/redo and sessions.
const wallId: string = componentHandle; // safe to store and reuse later

// DO: send `handle.id` (not the Handle object) to your popup UI or logs.
sendToUI("preview", { snapPointId: snapPoint.id });

// DON'T: construct or parse handle strings yourself.
const fake = "vec3_abc123"; // ✗ rejects with HANDLE_INVALID host-side

// DON'T: persist or reuse BREP topology handles after undo/redo/re-topologize.
// Re-fetch via design.query.geometry.getBrep and walk again instead.

// DON'T: expect mutation — every value-handle operation returns a NEW handle.