Appearance
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
Handleobject only means something inside your worker. Crossing any non-RPC boundary —sendToUIto your popup, persistence, logging — sendhandle.id. (Structured clone strips the class; a stripped handle is accepted back by the API as a mercy rule, but.idis 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.
| Handle | Kind prefix | What it references |
|---|---|---|
Vec3Handle | vec3_ | A 3D vector |
QuatHandle | quat_ | A quaternion (rotation) |
LineHandle | line_ | A line segment curve |
ArcHandle | arc_ | An arc curve |
CurveHandle | line_ 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) |
CircleHandle | circle_ | 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.
| Handle | Kind prefix | What it references |
|---|---|---|
ProfileHandle | profile_ | A host-resident profile |
ContourHandle | contour_ | 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.
| Handle | Kind prefix | What it references |
|---|---|---|
BrepHandle | brep_ | The boundary representation of a mesh |
FaceHandle | face_ | A face of a brep |
EdgeHandle | edge_ | An edge of a brep |
HalfEdgeHandle | halfedge_ | A directed half-edge of a brep (kind token is all-lowercase halfedge) |
VertexHandle | vertex_ | 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.
| Handle | Runtime form | What it references |
|---|---|---|
ComponentHandle | Raw 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 |
MaterialHandle | Raw 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.deletestill works but is deprecated in favor ofcore.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.getBrepand 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:
| Record | Shape | Returned 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.