Skip to content

Geometry Create ​

Curve creation — construct new geometric curves from point handles (all-handle model). Each method takes point handles (Vec3Handle) and returns an opaque curve handle (LineHandle, ArcHandle or SplineHandle) that you pass to query, update, or entity-creation methods. Read geometry back as plain values via the snaptrude.core.geom.query.* methods. Accessed via snaptrude.core.geom.create.

The brepFrom* constructors mint solid B-reps. Curved surfaces are exact, never faceted: brepFromSphere, brepFromCylinder, brepFromCone and brepFromTorus mint the analytic primitive directly, and sweep, revolution and extrusion carry whatever surface the kernel produces — a revolved arc is a real spherical or toroidal face, not a fan of triangles. Confirm it with core.geom.query.face.getSurfaceKind: every face of a 200-triangle "dome" answers "plane", a real dome's faces answer "sphere". Never fake a curve by scaling a mass — a scaled sphere is an ellipsoid.

Curved inputs are accepted throughout. Booleans, brepsFromSplit and the whole fillet family (fillet, chamfer, offset, shell) all take a curved solid, and fillet/chamfer take curved edges — a cylinder's rim, a dome's spring line — plus adjacent edges (corner blends). What they refuse is an edge with no crease: a seam of a closed face, or an edge between two patches of one smooth surface. (Twisted or structure-mismatched straight-edged lofts build with triangulated planar side faces — planar triangles, not approximation.)

Masses committed from a recipe constructor — the four primitives, extrusion, revolution, loft, sweep, sweep along a curve — carry a creation record you can read back with design.query.getParameters and edit with design.update.parameters. A fillet, chamfer, shell or offset of a recorded solid keeps the record too, with that step appended to its operations list — a rounded column stays editable. Booleans, split parts, patches, pieces cut by a face, a sweep carried by a rail, brepFromFaces, brepFromMesh — and anything built on top of one of those — carry none: getParameters answers null, and the label you pass to massFromBrep is the only provenance the mass has.

Functions ​

line(startPoint, endPoint) ​

Create a straight line segment between two point handles. Host API call — returns a handle.

  • Parameters:
    • startPoint: Vec3Handle — Start point of the line segment
    • endPoint: Vec3Handle — End point of the line segment
  • Returns: LineHandle — The new line
ts
const line = await snaptrude.core.geom.create.line(startPoint, endPoint);

arc(startPoint, endPoint, centrePoint, axis) ​

Create a circular arc from its start, end, centre, and axis point handles. Host API call — returns a handle.

  • Parameters:
    • startPoint: Vec3Handle — Start point of the arc
    • endPoint: Vec3Handle — End point of the arc
    • centrePoint: Vec3Handle — Centre point of the arc
    • axis: Vec3Handle — Axis direction of the arc
  • Returns: ArcHandle — The new arc
ts
const arc = await snaptrude.core.geom.create.arc(startPoint, endPoint, centrePoint, axis);

circle(centrePoint, axis, radius) ​

Create a circle from a centre point, an axis (plane normal), and a radius. Host API call — returns a handle. A circle is a closed curve; read it back via snaptrude.core.geom.query.circle.*.

  • Parameters:
    • centrePoint: Vec3Handle — Centre point of the circle
    • axis: Vec3Handle — Axis direction (plane normal) of the circle
    • radius: number — Radius of the circle
  • Returns: CircleHandle — The new circle
ts
const circle = await snaptrude.core.geom.create.circle(centrePoint, axis, 2);

splineFromPoints(points) ​

Create an open NURBS curve passing exactly through each point, in order (centripetal Catmull-Rom fit → clamped cubic B-spline; C1-smooth, with no cusps on uneven spacing). Host API call — returns a handle. The points are interpolated, not control points. For a closed smooth loop use profileFromSplinePoints — closed splines exist only as profiles, never as a single curve handle.

Pure host-side math — the geometry kernel is not loaded. The result is a CurveHandle: pass it to profileFromCurves alongside lines and arcs, or read it back via snaptrude.core.geom.query.curve.* (see the spline support table on Query: Curve).

  • Parameters:
    • points: Vec3Handle[] — 3..256 ordered through-points; consecutive points must be distinct
  • Returns: SplineHandle — The new open curve
  • Throws: VALIDATION if there are fewer than 3 points (2 points are a straight line — use line), more than 256 points, or two consecutive points coincide
ts
const spline = await snaptrude.core.geom.create.splineFromPoints([p0, p1, p2, p3]);

// Mixed profile: smooth top, straight closing base
const base = await snaptrude.core.geom.create.line(p3, p0);
const profile = await snaptrude.core.geom.create.profileFromCurves([spline, base]);

splineFromControlPoints(controlPoints, options?) ​

Create an open NURBS curve defined by control points — the curve is pulled toward the interior points, not through them; it starts at the first point and ends at the last. Knots are computed for you (clamped, uniform): there is no knot-vector argument. For a curve that must pass through given points use splineFromPoints.

A curve made here records as splinePoles (an exact NURBS definition — see CurveRecord) and is re-minted verbatim on edit, so poles read back off a mass can be moved and the mass rebuilt with design.update.parameters. Pure host-side math — the geometry kernel is not loaded and nothing is created in the scene.

  • Parameters:
    • controlPoints: Vec3Handle[] — 3..512 control points, in order
    • options.degree: number (optional) — 2 or 3; default min(3, controlPoints.length - 1)
  • Returns: SplineHandle — The new open curve
  • Throws: VALIDATION if fewer than degree + 1 points are given, two consecutive points coincide, or degree is neither 2 nor 3
ts
const curve = await snaptrude.core.geom.create.splineFromControlPoints([p0, p1, p2, p3]);

// Quadratic instead of the default cubic
const tighter = await snaptrude.core.geom.create.splineFromControlPoints([p0, p1, p2, p3], {
  degree: 2
});

curvesFromProjection(curve, face, faceBrep) ​

Drops a curve onto a face along the face's own normal and returns the curves it lands on — one per connected run. Host API call — returns new curve handles; the inputs are read-only. Nothing is created in the scene and the runs carry no recipe, exactly like a patch. The drop direction is measured from the curve's midpoint to the nearest point of the face's untrimmed surface, and the kernel drops the curve both ways along it, so a curve above a sphere lands on the near faces and mirrored on the far ones. faceBrep is the solid face belongs to, owner last — a face handle cannot name its own solid.

A curve that lands nowhere on the face is not an error: the call returns []. Use it to divide a face — close the runs into a contour (profileFromCurves → contourFromProfile), extrude it past the mass (brepFromExtrusion) and cut with brepsFromSplitByFace or brepFromSubtraction — or read the runs back through core.geom.query.curve.*.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

  • Parameters:
  • Returns: CurveHandle[] — One curve per connected run; [] when the curve lands nowhere on the face
  • Throws: VALIDATION if face is not a face of faceBrep, or the curve's midpoint lies on the face's (untrimmed) surface (there is no drop direction there — move or split the curve so its midpoint is clear of it); OPERATION_FAILED if the kernel declined the projection or its result could not be read back
ts
// Drop a straight run onto a column's barrel — it wraps to the full circle,
// because the drop runs both ways along the surface normal.
const column = await snaptrude.core.geom.create.brepFromCylinder(
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 1, z: 0 },
  2,
  6
);
const faces = await snaptrude.core.geom.query.brep.listFaces(column);
let barrel;
for (const face of faces) {
  if ((await snaptrude.core.geom.query.face.getSurfaceKind(face)) === "cylinder") barrel = face;
}
const runs = await snaptrude.core.geom.create.curvesFromProjection(line, barrel, column);
// runs.length === 0 when the line misses the barrel entirely

profileFromLinePoints(points) ​

Create a closed profile from an ordered list of point handles connected by line segments (last auto-connected to first). Host API call — returns a handle.

  • Parameters:
    • points: Vec3Handle[] — Ordered vertex handles of the profile
  • Returns: ProfileHandle — The new profile
ts
const profile = await snaptrude.core.geom.create.profileFromLinePoints(points);

profileFromCurves(curves) ​

Create a profile from an ordered list of curve handles forming a closed loop. Host API call — returns a handle. Lines, arcs and splines may be mixed freely — this is the route to a part-curved, part-straight outline.

  • Parameters:
    • curves: CurveHandle[] — Ordered curve handles forming a closed loop
  • Returns: ProfileHandle — The new profile
ts
const profile = await snaptrude.core.geom.create.profileFromCurves([line, arc]);

profileFromSplinePoints(points) ​

Create a closed smooth profile passing through each point, in order, with the loop closed smoothly back from the last point to the first (do not repeat the first point). Host API call — returns a handle. This is the closed counterpart of splineFromPoints and the one-call route from points to an extrudable outline: wrap it in contourFromProfile and extrude, loft or sweep as usual.

The whole loop is smooth — v1 takes no mixed straight/spline input. For a profile that is part curve and part straight, build the pieces with splineFromPoints + line and join them with profileFromCurves.

  • Parameters:
    • points: Vec3Handle[] — 3..256 ordered through-points of the loop (first point not repeated)
  • Returns: ProfileHandle — The new closed profile
  • Throws: VALIDATION if there are fewer than 3 points, more than 256 points, consecutive points coincide (including last → first), or the loop self-intersects
ts
// An organic podium plate through six plan points
const profile = await snaptrude.core.geom.create.profileFromSplinePoints([p0, p1, p2, p3, p4, p5]);
const contour = await snaptrude.core.geom.create.contourFromProfile(profile);
const brep = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3);
const podium = await snaptrude.design.create.massFromBrep(brep, "Podium");

profileRect(width, depth, center?) ​

Create an axis-aligned rectangle profile (XZ plane) of width × depth, centred at center (default origin). Pair with design.create.space / design.create.spaces to author a rectangular (box) space.

  • Parameters:
    • width: number — Extent along X
    • depth: number — Extent along Z
    • center: Vec3Handle (optional, default origin) — Rectangle centre
  • Returns: ProfileHandle — The new rectangle profile
ts
const rect = await snaptrude.core.geom.create.profileRect(4, 3);
const contour = await snaptrude.core.geom.create.contourFromProfile(rect);
const space = await snaptrude.design.create.space(contour, 3);

contourFromProfile(outer) ​

Create a contour (an outer profile loop, no holes). Host API call — returns a handle.

ts
const contour = await snaptrude.core.geom.create.contourFromProfile(outer);

contourFromProfiles(outer, holes?) ​

Create a contour from an outer profile plus zero or more inner profiles (holes). Host API call — returns a handle.

  • Parameters:
    • outer: ProfileHandle — The outer boundary loop
    • holes: ProfileHandle[] (optional) — Optional inner hole loops
  • Returns: ContourHandle — The new contour
ts
const contour = await snaptrude.core.geom.create.contourFromProfiles(outer, [hole]);

brepFromFaces(faces) ​

Create a closed solid B-rep from explicit face loops. Faces are plain arrays of {x, y, z} point components, NOT point handles (bulk-data precedent: design.query.geometry.getTriangulatedMeshes), in raw Babylon units.

Each face is one planar loop of ≥3 points; ≥4 faces are required. Loops may be authored in any consistent winding — the host validates edge coherence (every edge shared by exactly two faces, in opposite directions) and fixes the global orientation so faces point outward. Faces must be planar and the solid must be closed; holes in faces are not supported (v1).

Inspect the result via core.geom.query.brep.*, or commit it to the scene with design.create.massFromBrep.

  • Parameters:
    • faces: { x, y, z }[][] — Face loops, each an ordered array of points (≥3 points per face, ≥4 faces)
  • Returns: BrepHandle — The new solid
  • Throws: If a face is degenerate or non-planar, an edge is not shared by exactly two faces (open shell, non-manifold, or inconsistent winding), or the faces do not form a valid closed solid
ts
// A pyramid: square base + 4 triangular sides
const apex = { x: 0, y: 4, z: 0 };
const a = { x: -2, y: 0, z: -2 };
const b = { x: 2, y: 0, z: -2 };
const c = { x: 2, y: 0, z: 2 };
const d = { x: -2, y: 0, z: 2 };
const brep = await snaptrude.core.geom.create.brepFromFaces([
  [a, d, c, b], // base
  [a, b, apex],
  [b, c, apex],
  [c, d, apex],
  [d, a, apex]
]);
const faceCount = await snaptrude.core.geom.query.brep.getFaceCount(brep); // 5

brepFromExtrusion(contour, direction, amount, options?) ​

Create a closed solid B-rep by extruding a contour along a direction. The direction is normalised by the host, so amount is the extrusion distance in raw Babylon units (negative extrudes the opposite way). The contour is copied — the input handle is never mutated. Holes and arc profiles extrude natively (an arc/circle cross-section yields a cylinder).

Twist and taper via options, both about direction through the base contour's centre of area: twistInDegrees is the total rotation base → top in degrees (an angle — never scaled by the project's length unit), positive turning the top clockwise seen from the far end of direction looking back at the base — for the usual upward extrusion, the plan view — topScale the top outline's scale as a ratio (1 = no taper), and levels the number of sections the solid is built through, a count (2..64). Omit the bag entirely and this is the straight extrusion it has always been, byte for byte — a straight extrusion records exactly { contour, direction, amount }. A twisted or tapered one stays kind: "extrusion"; it never becomes a loft.

levels is host-owned: leave it out and the host resolves one and stamps it into the record, so the mass regenerates identically ever after, and a later design.update.parameters edit that changes the angle re-resolves it. A patch that changes levels is refused, and a first-twist patch that carries it at all is refused as a key the straight record does not have — send the twist and the taper, not the section count.

A twist or taper builds through the kernel, so this call returns a promise there; a straight extrusion is still synchronous host-side math.

  • Parameters:
    • contour: ContourHandle — The cross-section to extrude (outer profile + optional holes)
    • direction: { x, y, z } — Extrusion direction as plain components (non-zero; normalised by the host)
    • amount: number — Extrusion distance (non-zero; negative extrudes opposite to direction)
    • options: { twistInDegrees?, topScale?, levels? } (optional) — twistInDegrees an angle in degrees (finite), topScale a positive finite ratio, levels an integer 2..64
  • Returns: BrepHandle — The new solid
  • Throws: If the extrusion is degenerate (zero amount, zero direction, or a direction lying in the contour plane) or the contour cannot be extruded into a valid solid; VALIDATION if twistInDegrees is not finite or turns more than 1440°, topScale is not a positive finite ratio, levels is not an integer in 2..64 or is too few for the twist (more than 22.5° per step), a twist is asked of a direction that is not the contour's normal, the contour has holes, or a scaled section self-intersects
ts
// A 4 × 3 × 3 box
const rect = await snaptrude.core.geom.create.profileRect(4, 3);
const contour = await snaptrude.core.geom.create.contourFromProfile(rect);
const box = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3);
await snaptrude.core.geom.query.brep.getFaceCount(box); // 6

// A cylinder: circle authored as two semicircular arcs (radius 2, height 5)
const c = await snaptrude.core.math.vec3.new(0, 0, 0);
const up = await snaptrude.core.math.vec3.new(0, 1, 0);
const p1 = await snaptrude.core.math.vec3.new(2, 0, 0);
const p2 = await snaptrude.core.math.vec3.new(-2, 0, 0);
const arc1 = await snaptrude.core.geom.create.arc(p1, p2, c, up);
const arc2 = await snaptrude.core.geom.create.arc(p2, p1, c, up);
const circleProfile = await snaptrude.core.geom.create.profileFromCurves([arc1, arc2]);
const circleContour = await snaptrude.core.geom.create.contourFromProfile(circleProfile);
const cylinder = await snaptrude.core.geom.create.brepFromExtrusion(
  circleContour,
  { x: 0, y: 1, z: 0 },
  5
);
await snaptrude.core.geom.query.brep.getFaceCount(cylinder); // 4 — top, bottom, 2 cylindrical sides

// A twisted, tapered tower: 90° base → top, top outline 60 % of the footprint.
// No `levels` — the host resolves the section count and records it.
const plate = await snaptrude.core.geom.create.contourFromProfile(
  await snaptrude.core.geom.create.profileRect(10, 10)
);
const tower = await snaptrude.core.geom.create.brepFromExtrusion(plate, { x: 0, y: 1, z: 0 }, 30, {
  twistInDegrees: 90,
  topScale: 0.6
});
await snaptrude.design.create.massFromBrep(tower, "Twisted tower"); // still an `extrusion` record

brepFromPatch(boundary, thickness, options?) ​

Stretches a surface across a closed loop of boundary curves and thickens it into a solid in one call — a roof over four curves, a canopy between two towers, a blend across a gap. There is no surface object in Snaptrude: only solids reach the canvas, so thickness is required and there is nothing to keep afterwards but the solid. This — not a two-rail sweep — is the route to a canopy spanning two curved rails.

The boundary is 3..16 items in loop order forming one closed ring (ends meeting within tolerance). An item is a curve handle, or an edge of an existing solid given as { edge, brep, face? } — an edge handle cannot name its own solid, and one patch may meet two different masses. Read scene solids with design.query.geometry.getBrep(x, { frame: "world" }) so every item is in one frame.

continuity: "tangent" leaves each boundary smoothly, and then every item must be an edge item that also names the face to continue: a solid edge has two faces — the wall, or the top of the rim — and only the caller knows which. interiorPoints pull the surface through given points; side picks which way the solid grows from the surface ("below" by default).

The result carries no recipe — design.query.getParameters returns null and the mass cannot be edited through design.update.parameters. Pass a real label to massFromBrep ("Roof patch"): the label is all the provenance there is. Afterwards, name a part of it by a face (face.getArea / face.getCentroid), never by a face index you assumed.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

  • Parameters:
    • boundary: (CurveHandle | { edge, brep, face? })[] — 3..16 items forming one closed loop, in loop order
    • thickness: number — Solid thickness (positive, finite)
    • options: { interiorPoints?, continuity?, side? } (optional) — interiorPoints: up to 64 plain { x, y, z } in the boundary's frame, which the surface is pulled through; continuity: "position" (default) or "tangent"; side: "below" (default) or "above"
  • Returns: BrepHandle — The thickened patch as a new solid
  • Throws: VALIDATION if the boundary is not one closed loop (the message names the gap and its size in mm), a count cap is exceeded, thickness is not positive, continuity: "tangent" is given with a curve item or with an edge item that names no face (the message names the edge's two candidate faces), or a face is not adjacent to its edge; OPERATION_FAILED if the kernel could not fill the boundary, the filled surface misses its own boundary (the message carries the measured gap), or the solid it built cannot be read back into the model ("the result could not be read back — the solid the kernel built does not translate back into the model") — a boundary fitted through many points is the usual cause, so prefer arcs or fewer fit points
ts
// A roof over four boundary curves, 250 mm thick
const roof = await snaptrude.core.geom.create.brepFromPatch(
  [curveA, curveB, curveC, curveD],
  0.984 // 250 mm in internal units
);
await snaptrude.design.create.massFromBrep(roof, "Roof patch"); // the label is the provenance

// A canopy between two towers' rims, leaving every wall it meets smoothly:
// under `tangent` EVERY item is an edge naming the face to continue.
const canopy = await snaptrude.core.geom.create.brepFromPatch(
  [
    { edge: rimA, brep: towerA, face: wallA },
    { edge: sideA, brep: towerA, face: endA },
    { edge: rimB, brep: towerB, face: wallB },
    { edge: sideB, brep: towerB, face: endB }
  ],
  1.18,
  { continuity: "tangent" }
);

brepFromLoft(bottomContour, topContour, intermediateContours?, options?) ​

Create a closed solid B-rep by lofting between a bottom and a top contour, optionally through intermediate cross-sections. Sections may have different edge counts: Snaptrude deterministically inserts vertices on the smaller sections at the perimeter positions that best line up with the largest section's corners, then lofts as usual (multi-section lofts normalize every section to the largest edge count in one global pass). For straight-edged sections the seam is auto-aligned too — the corner-to-corner correspondence that avoids a twist is chosen for you, so sections drawn from different starting corners loft cleanly; arc-bearing sections keep the authored correspondence. When no planar quad correspondence exists at all — a rotated or twisted top, or sections whose edge directions differ (square to hexagon) — hole-free straight-edged pairs still build: planar side quads stay quads and each warped one splits into two triangles (triangles are always planar), so the loft succeeds with a faceted-side solid instead of throwing. Arc-bearing or holed pairs keep the planar-quad requirement (matching hole counts allowed in the two-section form) and throw on twist. With intermediates the loft is a chain of ruled segments folded into one solid: sections must be planar and hole-free, ordered bottom → intermediates → top. The contours are copied — the input handles are never mutated.

Both of those automatic behaviors are switchable via options (each defaults to "auto", the behavior above):

  • compatibility: "strict" — refuse mismatched edge counts instead of auto-matching. The check runs before any vertex insertion: if outer edge counts differ across sections (or, in the two-section form, a paired hole's edge counts differ), the loft throws VALIDATION naming the sections and their counts, and no normalization runs. With equal counts the result is identical to "auto".
  • seamAlignment: "authored" — keep the authored start-vertex correspondence instead of searching seam rotations, so corners pair exactly as drawn. A pair that only lofts cleanly under a rotated seam then builds the drawn (twisted) correspondence through the triangulated lane instead of being rescued.

{ compatibility: "strict", seamAlignment: "authored" } together reproduce pre-0.9.4 lofting exactly.

A third switch changes the sides rather than the matching: smoothing: "smooth" lofts every section in one kernel pass, so the sides curve continuously through the intermediates instead of creasing at each one — a vase, a curved tower. The sections must still be planar and hole-free (a hole has nowhere to go in a one-wire-per-section loft, and is refused). The default "ruled" is the straight-sided chain described above, byte for byte.

  • Parameters:
    • bottomContour: ContourHandle — The bottom cross-section
    • topContour: ContourHandle — The top cross-section (edge counts may differ — vertices are auto-inserted to match)
    • intermediateContours: ContourHandle[] (optional) — In-between cross-sections, ordered bottom to top (hole-free; edge counts may differ)
    • options: { compatibility?, seamAlignment?, smoothing? } (optional) — Behavior switches: compatibility is "strict" (throw on mismatched edge counts) or "auto" (default — insert vertices to match); seamAlignment is "authored" (keep the authored corner correspondence) or "auto" (default — search seam rotations on straight-edged sections); smoothing is "smooth" (one kernel pass, sides curving through the intermediates) or "ruled" (default — straight-sided segments)
  • Returns: BrepHandle — The new solid
  • Throws: VALIDATION if the contours are coincident, their hole counts differ, auto-matching would pair an arc with a straight edge (or with a non-congruent arc), a section has holes under smoothing: "smooth", any section has holes when intermediates are present (multi-section lofts take hole-free sections), a section is non-planar, or an arc-bearing/holed pair has a non-planar side face (straight-edged hole-free pairs triangulate instead of throwing); with compatibility: "strict", if section edge counts (or hole-pair edge counts) differ — the error names the sections and counts and its hint points at compatibility: "auto"; OPERATION_FAILED if the lofted segments cannot be joined into a valid solid
ts
// A square frustum: 4m base lofted to a 2m top, 3m up
const base = await snaptrude.core.geom.create.profileRect(4, 4);
const top = await snaptrude.core.geom.create.profileRect(
  2,
  2,
  await snaptrude.core.math.vec3.new(0, 3, 0)
);
const frustum = await snaptrude.core.geom.create.brepFromLoft(
  await snaptrude.core.geom.create.contourFromProfile(base),
  await snaptrude.core.geom.create.contourFromProfile(top)
);
await snaptrude.core.geom.query.brep.getFaceCount(frustum); // 6

// A three-section loft: base → mid → top (hole-free; edge counts may differ)
const mid = await snaptrude.core.geom.create.profileRect(
  3,
  3,
  await snaptrude.core.math.vec3.new(0, 1.5, 0)
);
const tapered = await snaptrude.core.geom.create.brepFromLoft(
  await snaptrude.core.geom.create.contourFromProfile(base),
  await snaptrude.core.geom.create.contourFromProfile(top),
  [await snaptrude.core.geom.create.contourFromProfile(mid)]
);
await snaptrude.design.create.massFromBrep(tapered, "tapered tower");

// The same three sections with sides that curve through the middle one
const vase = await snaptrude.core.geom.create.brepFromLoft(
  await snaptrude.core.geom.create.contourFromProfile(base),
  await snaptrude.core.geom.create.contourFromProfile(top),
  [await snaptrude.core.geom.create.contourFromProfile(mid)],
  { smoothing: "smooth" }
);

// Exact authored lofting: throw on mismatched edge counts, keep the drawn seam
const exact = await snaptrude.core.geom.create.brepFromLoft(
  await snaptrude.core.geom.create.contourFromProfile(base),
  await snaptrude.core.geom.create.contourFromProfile(top),
  undefined,
  { compatibility: "strict", seamAlignment: "authored" }
);

brepFromMesh(positions, faces) ​

Create a closed solid B-rep from indexed mesh data: a vertex position array plus face loops of indices into it. The indexed form of brepFromFaces — same validation (planar faces, closed manifold solid, every edge shared by exactly two faces) after the indices are expanded to point loops.

  • Parameters:
    • positions: { x, y, z }[] — Vertex positions (≥4)
    • faces: number[][] — Face loops, each an ordered array of indices into positions (≥3 indices per face, ≥4 faces)
  • Returns: BrepHandle — The new solid
  • Throws: If an index is not an integer within positions bounds, a face is degenerate or non-planar, or the faces do not form a valid closed solid
ts
// A tetrahedron from 4 vertices and 4 triangular faces
const positions = [
  { x: 0, y: 0, z: 0 },
  { x: 4, y: 0, z: 0 },
  { x: 2, y: 0, z: 4 },
  { x: 2, y: 3, z: 1.5 }
];
const tetra = await snaptrude.core.geom.create.brepFromMesh(positions, [
  [0, 2, 1], // base
  [0, 1, 3],
  [1, 2, 3],
  [2, 0, 3]
]);
await snaptrude.core.geom.query.brep.getVertexCount(tetra); // 4

brepFromUnion(a, b) ​

Create a closed solid B-rep as the boolean union of two breps (a ∪ b). Inputs are read-only and may be authored breps (any core.geom.create brep constructor) or scene-derived breps from design.query.geometry.getBrep; coordinates are combined as-is, so both inputs must share a frame. The result must be a single solid — disjoint inputs are rejected.

The first boolean call loads the OpenCascade geometry kernel (a wasm of tens of MB — expect a pause of seconds); subsequent calls are fast.

  • Parameters:
  • Returns: Promise<BrepHandle> — The union as a new solid
  • Throws: If the inputs do not overlap or touch (the union would be disjoint solids) or the result cannot be built as a valid single solid
ts
// Two overlapping 4×4×3 boxes → one L-shaped solid
const boxAt = async (cx, cz, baseY = 0) => {
  const centre = await snaptrude.core.math.vec3.new(cx, baseY, cz);
  const rect = await snaptrude.core.geom.create.profileRect(4, 4, centre);
  const contour = await snaptrude.core.geom.create.contourFromProfile(rect);
  return snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3);
};
const merged = await snaptrude.core.geom.create.brepFromUnion(
  await boxAt(0, 0),
  await boxAt(2, 2, 1)
);

brepFromSubtraction(a, b) ​

Create a closed solid B-rep as the boolean subtraction of two breps: a minus b — b is cut away from a. Argument order matters. Inputs are read-only and may be authored or scene-derived breps (see brepFromUnion); coordinates are combined as-is.

The first boolean call loads the OpenCascade geometry kernel (a wasm of tens of MB — expect a pause of seconds); subsequent calls are fast.

  • Parameters:
  • Returns: Promise<BrepHandle> — a minus b as a new solid
  • Throws: If b consumes a entirely (empty result), the result splits into disjoint solids, or it cannot be built as a valid single solid
ts
// Carve the courtyard out of the building mass — building minus courtyard
const carved = await snaptrude.core.geom.create.brepFromSubtraction(buildingBrep, courtyardBrep);

brepFromIntersection(a, b) ​

Create a closed solid B-rep as the boolean intersection of two breps (a ∩ b — the shared volume only). Inputs are read-only and may be authored or scene-derived breps (see brepFromUnion); coordinates are combined as-is.

The first boolean call loads the OpenCascade geometry kernel (a wasm of tens of MB — expect a pause of seconds); subsequent calls are fast.

  • Parameters:
  • Returns: Promise<BrepHandle> — The shared volume as a new solid
  • Throws: If the inputs do not overlap (empty intersection), the result splits into disjoint solids, or it cannot be built as a valid single solid
ts
// Clip the tower to the zoning envelope
const clipped = await snaptrude.core.geom.create.brepFromIntersection(towerBrep, envelopeBrep);

brepFromFillet(brep, edges, radius, options?) ​

Rounds the given edges of a solid with a constant radius. Returns a new brep; the input brep is read-only.

Works on curved solids: edges may be straight, circular or spline, and adjacent edges (corner blends) are allowed. To pick a rim — the circle where a cylinder meets a flat cap — walk the edges and keep the ones whose two faces are a cylinder and a plane: core.geom.query.brep.listEdges(brep) → per edge core.geom.query.edge.listFaces(edge) → core.geom.query.face.getSurfaceKind on both faces. (listEdgesBetween takes two vertex indices — it is not a face-pair lookup.) For one cap's rim only, core.geom.query.face.listEdges(capFace) after a getSurfaceKind census of brep.listFaces is the shorter walk.

A variable blend — a round that widens along the edge — comes from options, in one of two mutually exclusive forms, each naming exactly one edge: the law belongs to the whole tangent chain that edge lies on, and is measured from that edge's start.

  • endRadius — the radius at the far end, interpolated linearly along the chain. It needs an open chain with two distinct ends, so on a closed rim it is refused unless it equals radius (the kernel would silently build a constant radius instead) — the message points at radii.
  • radii — a sampled law: 2..32 rows of { position, radius } with strictly increasing position. position is normalised arc length along the resolved chain — a ratio in 0..1, never a length and never converted; radius is a length. On a closed rim 0 and 1 are the same point, so give them the same radius and put the variation in the interior rows.

Omit the bag and the fillet is the constant-radius one it has always been — radius is then the whole blend, and many edges are still accepted.

When the input brep carries a creation recipe, the result carries it too, with this operation appended to operations — design.create.massFromBrep then keeps the mass editable through design.update.parameters; a variable blend's law rides the same record.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

Inspect the result via core.geom.query.brep.*, or commit it to the scene with design.create.massFromBrep.

  • Parameters:
    • brep: BrepHandle — The solid whose edges to round
    • edges: EdgeHandle[] — The edges to fillet (one or more, all on brep, no duplicates; more than 64 still fillets but the result carries no parameter record). Exactly one when a variable form is given
    • radius: number — Fillet radius (positive, finite) — the start radius under endRadius; under radii the table's first row is the start and radius is not part of the law (it must still be positive)
    • options: { endRadius?, radii? } (optional) — endRadius a positive finite length; radii 2..32 rows of { position, radius }, position a ratio 0..1 strictly increasing. Mutually exclusive
  • Returns: BrepHandle — The filleted solid as a new brep
  • Throws: VALIDATION if edges is empty or holds a duplicate, radius is not a positive finite number, an edge is not on brep, or an edge has no crease to round — it is a seam of a closed face, or it lies between two patches of one smooth surface; for a variable blend, if endRadius and radii are both given, more than one edge is passed, a radii table is outside 2..32 rows or its positions do not strictly increase within 0..1, or the law's two ends differ on a closed rim (an endRadius other than radius, or a radii table whose first and last radii differ); PRECONDITION_FAILED if an edge could not be located on the kernel solid after healing (select a different edge, or fillet before the operation that curved the solid) or the kernel could not blend the edge the message names (reduce the radius or drop that edge); OPERATION_FAILED if the kernel or the translation fails, or the result is non-manifold, splits into several solids, or contains a surface Snaptrude cannot represent
ts
// Build a 4 × 3 × 3 box, round one edge, commit as a mass
const rect = await snaptrude.core.geom.create.profileRect(4, 3);
const contour = await snaptrude.core.geom.create.contourFromProfile(rect);
const box = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3);
const edges = await snaptrude.core.geom.query.brep.listEdges(box);
const rounded = await snaptrude.core.geom.create.brepFromFillet(box, [edges[0]], 0.3);
await snaptrude.design.create.massFromBrep(rounded, "rounded box");

// Round a column's rim: every edge between the cylinder side and a plane cap
const column = await snaptrude.core.geom.create.brepFromCylinder(
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 1, z: 0 },
  0.6,
  6
);
const columnEdges = await snaptrude.core.geom.query.brep.listEdges(column);
const rim = [];
for (const edge of columnEdges) {
  const faces = await snaptrude.core.geom.query.edge.listFaces(edge);
  const kinds = [];
  for (const face of faces) {
    kinds.push(await snaptrude.core.geom.query.face.getSurfaceKind(face));
  }
  if (kinds.includes("cylinder") && kinds.includes("plane")) rim.push(edge);
}
const softened = await snaptrude.core.geom.create.brepFromFillet(column, rim, 0.2);
await snaptrude.design.create.massFromBrep(softened, "Column"); // still editable

// A blend that varies along ONE edge: 0.5 at its start, 2 halfway, 0.5 at its end.
// `position` is arc length along the chain as a ratio — never a length.
const varied = await snaptrude.core.geom.create.brepFromFillet(box, [edges[0]], 0.5, {
  radii: [
    { position: 0, radius: 0.5 },
    { position: 0.5, radius: 2 },
    { position: 1, radius: 0.5 }
  ]
});

// The same idea on an OPEN chain, as a straight run from 0.5 to 1.5
const tapered = await snaptrude.core.geom.create.brepFromFillet(box, [edges[0]], 0.5, {
  endRadius: 1.5
});

brepFromOffset(brep, distance) ​

Grows or shrinks a solid by offsetting every face — positive distance moves faces outward, negative moves them inward. Returns a new brep; the input brep is read-only.

Works on curved solids: a sphere offsets to a sphere, a torus to a torus, a cylinder to a cylinder. There is nothing to select — the whole solid is offset.

When the input brep carries a creation recipe, the result carries it too, with this operation appended to operations — design.create.massFromBrep then keeps the mass editable through design.update.parameters.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

Inspect the result via core.geom.query.brep.*, or commit it to the scene with design.create.massFromBrep.

  • Parameters:
    • brep: BrepHandle — The solid to offset
    • distance: number — Offset distance (finite, non-zero; positive grows, negative shrinks)
  • Returns: BrepHandle — The offset solid as a new brep
  • Throws: VALIDATION if distance is not finite, is too small to offset anything, or an inward distance consumes the solid entirely; OPERATION_FAILED if the kernel or the translation fails, or the result is non-manifold, splits into several solids, or contains a surface Snaptrude cannot represent — an offset free-form NURBS surface may have no exact representation, and the model is then left unchanged. A face the kernel left unmoved is logged, not refused (the per-face history check is log-only today), so a distance the kernel cannot honour surfaces as OPERATION_FAILED — use a smaller distance, or simplify the solid
ts
const rect = await snaptrude.core.geom.create.profileRect(4, 3);
const contour = await snaptrude.core.geom.create.contourFromProfile(rect);
const box = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3);
const grown = await snaptrude.core.geom.create.brepFromOffset(box, 0.5);
await snaptrude.design.create.massFromBrep(grown, "grown box");

// Thicken a torus ring by 100 mm — the offset ring is still a torus
const ring = await snaptrude.core.geom.create.brepFromTorus(
  { x: 0, y: 6, z: 0 },
  { x: 0, y: 1, z: 0 },
  5,
  1
);
const thicker = await snaptrude.core.geom.create.brepFromOffset(ring, 0.1);

brepFromShell(brep, openFaces, thickness) ​

Hollows a solid into constant-thickness walls, removing the given faces as openings. Returns a new brep; the input brep is read-only. The outer surface is kept and the walls grow inward.

Works on curved solids: the open faces may be spherical, cylindrical, conical, toroidal or free-form patches, not only flat ones. To open the flat base of a dome, census the faces first — core.geom.query.brep.listFaces then core.geom.query.face.getSurfaceKind on each — and keep the "plane" one. A closed curved surface is stored as several patches (a sphere as lunes), so opening "the sphere" may mean passing several faces.

When the input brep carries a creation recipe, the result carries it too, with this operation appended to operations — design.create.massFromBrep then keeps the mass editable through design.update.parameters.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

Inspect the result via core.geom.query.brep.*, or commit it to the scene with design.create.massFromBrep.

  • Parameters:
    • brep: BrepHandle — The solid to hollow
    • openFaces: FaceHandle[] — The faces to remove as openings (one or more, all on brep, no duplicates; more than 32 still shells but the result carries no parameter record)
    • thickness: number — Wall thickness (positive, finite, smaller than half the solid's smallest span)
  • Returns: BrepHandle — The hollowed solid as a new brep
  • Throws: VALIDATION if openFaces is empty or holds a duplicate, a face is not on brep, thickness is not a positive finite number, or the thickness is too large (it must be smaller than half the solid's smallest span); OPERATION_FAILED if the kernel or the translation fails, or the result is non-manifold, splits into several solids, or contains a surface Snaptrude cannot represent. A kept face the kernel did not thicken is logged, not refused (the per-face history check is log-only today), so a thickness the kernel cannot honour surfaces as OPERATION_FAILED — keep it below half the solid's smallest span, or open a larger face
ts
// Hollow a box into 200mm walls with one face open
const rect = await snaptrude.core.geom.create.profileRect(4, 3);
const contour = await snaptrude.core.geom.create.contourFromProfile(rect);
const box = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3);
const faces = await snaptrude.core.geom.query.brep.listFaces(box);
const hollow = await snaptrude.core.geom.create.brepFromShell(box, [faces[0]], 0.2);
await snaptrude.design.create.massFromBrep(hollow, "hollow box");

// Hollow a dome into a 300 mm shell, open at its flat base
const domeFaces = await snaptrude.core.geom.query.brep.listFaces(dome);
const base = [];
for (const face of domeFaces) {
  if ((await snaptrude.core.geom.query.face.getSurfaceKind(face)) === "plane") base.push(face);
}
const shell = await snaptrude.core.geom.create.brepFromShell(dome, base, 0.3);

brepsFromSplit(brep, planeOrigin, planeNormal) ​

Cuts a solid by an infinite plane and returns one brep per resulting piece — a single piece if the plane misses the solid. Returns new brep handles; the input brep is read-only. The plane is defined by a point on it and its normal direction.

Curved solids are accepted — this is the direct route from a sphere to a hemisphere or dome. The pieces keep their exact curved surfaces, but they carry no creation record: design.query.getParameters returns null for a split part, so pick this route when the shape matters more than later editability (revolving a quarter-circle keeps a record).

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

Inspect each result via core.geom.query.brep.*, or commit them to the scene with design.create.massFromBrep.

  • Parameters:
    • brep: BrepHandle — The solid to split
    • planeOrigin: { x, y, z } — A point on the cutting plane as plain components
    • planeNormal: { x, y, z } — The plane normal as plain components (non-zero)
  • Returns: BrepHandle[] — The resulting pieces (one per solid)
  • Throws: VALIDATION if planeNormal is zero-length or the origin/normal components are not finite; OPERATION_FAILED if the kernel cannot split the solid
ts
// Cut a 24m tower at 12m with a horizontal plane, commit each piece
const rect = await snaptrude.core.geom.create.profileRect(8, 8);
const contour = await snaptrude.core.geom.create.contourFromProfile(rect);
const tower = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 24);
const pieces = await snaptrude.core.geom.create.brepsFromSplit(
  tower,
  { x: 0, y: 12, z: 0 },
  { x: 0, y: 1, z: 0 }
);
for (const piece of pieces) {
  await snaptrude.design.create.massFromBrep(piece);
}

// A dome: cut a sphere at its equator and keep the upper half.
const ball = await snaptrude.core.geom.create.brepFromSphere({ x: 0, y: 0, z: 0 }, 6);
const halves = await snaptrude.core.geom.create.brepsFromSplit(
  ball,
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 1, z: 0 }
);
for (const half of halves) {
  const centroid = await snaptrude.core.geom.query.brep.getCentroid(half);
  if (centroid.y > 0) await snaptrude.design.create.massFromBrep(half, "dome");
}

brepsFromSplitByFace(brep, toolFace, toolBrep) ​

Cuts a solid with a face of another solid — the curved twin of brepsFromSplit, which cuts with a plane. Returns new brep handles; both inputs are read-only. This is how a mass is divided along a curved roof or shell surface.

Both breps must be in the same frame. Read scene masses with design.query.geometry.getBrep(x, { frame: "world" }) — the default is each mass's own local frame, and two masses have two of those, so a tool read in the wrong frame misses the target entirely. toolBrep is the solid toolFace belongs to, owner last: a face handle cannot name its own solid.

The pieces come back in descending volume order; pick the one you want with brep.getCentroid or brep.getVolume. A piece carries no recipe — label it when you commit it, exactly as for a patch.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

  • Parameters:
    • brep: BrepHandle — The solid to cut
    • toolFace: FaceHandle — The cutting face (read-only)
    • toolBrep: BrepHandle — The solid toolFace belongs to (owner last)
  • Returns: BrepHandle[] — The resulting pieces, largest first
  • Throws: VALIDATION if toolFace is not a face of toolBrep, toolBrep is brep, or the cut produced a single part — the surface does not pass all the way through (extend it past the mass on every side, and check both breps were read in the same frame); OPERATION_FAILED if the kernel could not split the solid
ts
// Cut a tower along a curved roof surface and keep the lower part
const target = await snaptrude.design.query.geometry.getBrep(tower, { frame: "world" });
const tool = await snaptrude.design.query.geometry.getBrep(roof, { frame: "world" });
const [toolFace] = await snaptrude.core.geom.query.brep.listFaces(tool);
const pieces = await snaptrude.core.geom.create.brepsFromSplitByFace(target, toolFace, tool);
const centroids = [];
for (const piece of pieces) {
  centroids.push(await snaptrude.core.geom.query.brep.getCentroid(piece));
}
const lower = pieces[centroids.indexOf(centroids.reduce((a, b) => (a.y <= b.y ? a : b)))];
await snaptrude.design.create.massFromBrep(lower, "Lower part"); // the label is the provenance

brepFromChamfer(brep, edges, distance) ​

Bevels the given edges of a solid with a symmetric planar cut. Returns a new brep; the input brep is read-only.

Works on curved solids: edges may be straight, circular or spline, and adjacent edges are allowed. Pick edges the same way as for brepFromFillet — brep.listEdges, then edge.listFaces and face.getSurfaceKind on both faces of each edge. (listEdgesBetween takes two vertex indices — it is not a face-pair lookup.) Chamfering a cylinder's rim yields a cone patch where a fillet would yield a torus patch.

When the input brep carries a creation recipe, the result carries it too, with this operation appended to operations — design.create.massFromBrep then keeps the mass editable through design.update.parameters.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

Inspect the result via core.geom.query.brep.*, or commit it to the scene with design.create.massFromBrep.

  • Parameters:
    • brep: BrepHandle — The solid whose edges to bevel
    • edges: EdgeHandle[] — The edges to chamfer (one or more, all on brep, no duplicates; more than 64 still chamfers but the result carries no parameter record)
    • distance: number — Chamfer distance from the edge on each adjacent face (positive, finite)
  • Returns: BrepHandle — The chamfered solid as a new brep
  • Throws: VALIDATION if edges is empty or holds a duplicate, distance is not a positive finite number, an edge is not on brep, or an edge has no crease to cut — it is a seam of a closed face, or it lies between two patches of one smooth surface; PRECONDITION_FAILED if an edge could not be located on the kernel solid after healing (select a different edge, or chamfer before the operation that curved the solid); OPERATION_FAILED if the kernel or the translation fails, or the result is non-manifold, splits into several solids, or contains a surface Snaptrude cannot represent. An edge the kernel did not bevel is logged, not refused (the per-edge history check is log-only, and MakeChamfer has no faulty-contour verdict of its own, unlike the fillet), so a distance the kernel cannot honour surfaces as OPERATION_FAILED — reduce it, or chamfer fewer edges
ts
const rect = await snaptrude.core.geom.create.profileRect(4, 3);
const contour = await snaptrude.core.geom.create.contourFromProfile(rect);
const box = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3);
const edges = await snaptrude.core.geom.query.brep.listEdges(box);
const beveled = await snaptrude.core.geom.create.brepFromChamfer(box, [edges[0]], 0.1);
await snaptrude.design.create.massFromBrep(beveled, "beveled box");

brepFromSweep(profile, path, options?) ​

Sweeps a planar profile along a polyline path, with mitred corners at each bend by default (chamfered corners via options.transition — see Corner transitions below). Returns a new brep; the input contour is read-only. The profile must be hole-free and must not lie in a plane containing the first path segment's direction.

Closed paths are supported: repeat the first point as the last and the sweep returns a closed ring solid (a picture frame, a duct loop) with planar mitred joints — no end caps. Closed paths must be planar with straight segments and ≥3 distinct corners, and the profile must be straight-edged (arcs would sweep into elliptical edges at the mitres). The ring's cross-section is the authored profile projected along the first leg onto the first mitre plane — a profile not perpendicular to the first leg is sheared, not rotated.

Taper via options: either startScale/endScale (linear interpolation by path length) or a per-vertex scales array (one entry per path point — the taper rate can change mid-run). Scales apply about the profile's own centroid. A uniform scale (all values equal) works wherever today's sweep works — the pre-scaled profile takes the exact same lane as an unscaled one. A varying scale takes straight open paths and straight-edged profiles only — the side faces are planar trapezoids; tapering across a bend or on an arc edge would create surfaces Snaptrude cannot represent, so it throws with the workaround: sweep each straight run separately with its own scales, then combine with brepFromUnion.

Corner transitions via options.transition: "miter" (the default) keeps the sharp mitred bend described above. "round" carries the section around each turning corner on a tangent circular arc, so the corner patch is a real curved surface rather than a flat cut — open paths only (a closed ring never touches the kernel, so "round" on one is refused with PRECONDITION_FAILED; sweep the open runs with rounded corners and union the pieces, or use "miter"/{ bevel } on the ring). { bevel: b } chamfers each turning corner by cutting b back along both adjacent legs — pure path preprocessing (the corner point is replaced by the two cut points, then the sweep runs as usual), so it works on open and closed paths and composes with a uniform scale; each leg must be long enough for the cuts its beveled corners take from it (b per adjacent beveled corner) and no corner may nearly double back (its chamfer would collapse), or the sweep throws naming the corner. On a corner-less (straight) path transition has no effect, and the varying-scale taper takes straight paths only, so it never sees a corner.

Open paths run on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds); closed rings and varying-scale tapers are computed host-side and never load the wasm.

Inspect the result via core.geom.query.brep.*, or commit it to the scene with design.create.massFromBrep.

  • Parameters:
    • profile: ContourHandle — The cross-section to sweep (hole-free contour)
    • path: { x, y, z }[] — The polyline path as plain points (≥2; repeat the first point last for a closed ring)
    • options: { startScale?, endScale?, scales?, transition? } (optional) — Taper: endpoint scales interpolate linearly by path length, or scales gives one factor per path point (mutually exclusive with the endpoint scales); each factor must be finite and between 0.001 and 1000. Corner transition: "miter" (default), "round" (arc-carried corners — curved corner patches; open paths only) or { bevel } (chamfer distance, finite and between 0.001 and 1000; open and closed paths)
  • Returns: BrepHandle — The swept solid as a new brep
  • Throws: PRECONDITION_FAILED for transition: "round" on a closed path (sweep open runs and union them, or use "miter" / { bevel }); VALIDATION if the profile has holes, the path has fewer than 2 points, consecutive path points coincide, a coordinate is not finite, the profile plane contains the first path segment's direction (degenerate sweep), a closed path is non-planar / doubles back / has fewer than 3 distinct corners, the profile is arc-bearing or too large for a mitred corner on a closed path, a varying scale is used on a closed path / across a bend / with an arc-bearing profile, a scale collapses the profile below modeling tolerance, or the scales array length differs from the path length; for "round" and { bevel }, if a path leg is too short for the cuts its transitioned corners take from it or a corner nearly doubles back so its transition collapses (the errors name the corner); OPERATION_FAILED if the kernel cannot sweep the profile into a valid solid
ts
// Sweep a 0.4 × 0.4 duct profile up a riser and along the ceiling.
// profileRect lies in the XZ plane, so the first path segment must
// leave that plane — start the path going up (+Y).
const rect = await snaptrude.core.geom.create.profileRect(0.4, 0.4);
const profile = await snaptrude.core.geom.create.contourFromProfile(rect);
const duct = await snaptrude.core.geom.create.brepFromSweep(profile, [
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 2.8, z: 0 },
  { x: 10, y: 2.8, z: 0 }
]);
await snaptrude.design.create.massFromBrep(duct, "duct");

// A closed rectangular ring — repeat the first point to close the loop.
const frameProfile = await snaptrude.core.geom.create.contourFromProfile(
  await snaptrude.core.geom.create.profileRect(0.2, 0.2)
);
const frame = await snaptrude.core.geom.create.brepFromSweep(frameProfile, [
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 3, z: 0 },
  { x: 4, y: 3, z: 0 },
  { x: 4, y: 0, z: 0 },
  { x: 0, y: 0, z: 0 }
]);
await snaptrude.core.geom.query.brep.getFaceCount(frame); // 16 — 4 legs × 4 edges, no caps

// A tapered column: 1 × 1 at the base, 2 × 2 at the top.
const columnProfile = await snaptrude.core.geom.create.contourFromProfile(
  await snaptrude.core.geom.create.profileRect(1, 1)
);
const column = await snaptrude.core.geom.create.brepFromSweep(
  columnProfile,
  [
    { x: 0, y: 0, z: 0 },
    { x: 0, y: 10, z: 0 }
  ],
  { startScale: 1, endScale: 2 }
);
await snaptrude.design.create.massFromBrep(column, "tapered column");

// Chamfered corners: each ring corner is cut back 0.5 along both legs.
const chamferedFrame = await snaptrude.core.geom.create.brepFromSweep(
  frameProfile,
  [
    { x: 0, y: 0, z: 0 },
    { x: 0, y: 3, z: 0 },
    { x: 4, y: 3, z: 0 },
    { x: 4, y: 0, z: 0 },
    { x: 0, y: 0, z: 0 }
  ],
  { transition: { bevel: 0.5 } }
);

// Rounded corners on an OPEN run — each bend becomes a real curved patch.
const handrail = await snaptrude.core.geom.create.brepFromSweep(
  profile,
  [
    { x: 0, y: 0, z: 0 },
    { x: 0, y: 1, z: 0 },
    { x: 6, y: 1, z: 0 }
  ],
  { transition: "round" }
);

brepFromSweepAlongCurve(profile, path, options?) ​

Sweeps a cross-section along a smooth path — splines, arcs and lines chained end to end — with control of how the profile turns. brepFromSweep takes a polyline of points; this takes curve handles. Returns a new brep; the inputs are read-only.

options.orientation is the frame law: "upright" (the default) keeps the profile's up aligned with world up — ducts, handrails, ribbons that must not roll; "frenet" lets it roll with the path's curvature; "fixed" carries the authored frame along unchanged, which is a pure translation — that is how you bend a footprint along an arc. There is no separate bend member. Because "upright" holds the section level, a path that rises sweeps the section area times the path's plan length, not its 3-D length, and "frenet", following the path's own frame, flips the section at an inflection of a spline fitted through points — keep "frenet" for genuinely three-dimensional paths.

The result records as kind: "sweepAlongCurve", so the orientation stays editable through design.update.parameters (the properties panel shows it read-only — a string is not a scalar row). One exception: options.rail, a second guide curve, makes a railed sweep one-shot — the record carries no rail, so rather than record a solid it could not regenerate, the host records nothing and getParameters answers null.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

  • Parameters:
    • profile: ContourHandle — Hole-free cross-section, placed at the first curve's start
    • path: CurveHandle[] — 1..64 curves, ordered, meeting end to end within tolerance
    • options: { orientation?, rail? } (optional) — orientation is "upright" (default), "frenet" or "fixed"; rail is a second guide CurveHandle (mutually exclusive with orientation)
  • Returns: BrepHandle — The swept solid as a new brep
  • Throws: VALIDATION if the path curves do not meet (the message names the gap), the path is closed, the profile has holes or is non-planar, the first tangent lies in the profile plane, the path runs vertically under "upright" (that frame is undefined there — the message names the run and points at "frenet"), the path turns parallel to the profile under "fixed", or a rail is given together with orientation or as a closed curve; OPERATION_FAILED if the kernel could not sweep the profile — a self-intersection on a tight bend
ts
// A duct along a curved route, kept level
const route = await snaptrude.core.geom.create.splineFromPoints(points);
const duct = await snaptrude.core.geom.create.brepFromSweepAlongCurve(profile, [route], {
  orientation: "upright"
});
await snaptrude.design.create.massFromBrep(duct, "Duct"); // editable: patch `options.orientation`

// Bend a footprint along an arc — `fixed` carries the authored frame, unrotated
const bent = await snaptrude.core.geom.create.brepFromSweepAlongCurve(footprint, [arc], {
  orientation: "fixed"
});

brepFromRevolution(profile, axisOrigin, axisDirection, angleInDegrees?) ​

Revolves a planar profile about an axis to make a solid of revolution — a full turn by default. Returns a new brep; the input contour is read-only. Holes in the profile are allowed, and segments may run at any angle to the axis: an inclined segment revolves into a cone, an arc into a sphere or torus section. This is the general route to domes, vases, columns and rotundas.

Two rules bind the axis to the profile. First, the axis line must lie in the profile's plane or run parallel to it — a revolution turns a planar section about a line drawn in that same plane, and an axis parallel to the plane but offset out of it sweeps a hollow ring; an axis skew to the plane is refused. Second, within that plane the whole profile stays on one side of the axis (for an offset parallel axis, of its projection into the plane). Vertices and whole segments may lie on the axis — that is how a dome's pole and a cylinder's inner edge are authored — but no part of the profile may cross to the other side, which would sweep a self-intersecting solid.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

Inspect the result via core.geom.query.brep.*, or commit it to the scene with design.create.massFromBrep.

  • Parameters:
    • profile: ContourHandle — The cross-section to revolve (planar contour, holes allowed)
    • axisOrigin: { x, y, z } — A point on the revolution axis as plain components
    • axisDirection: { x, y, z } — The axis direction as plain components (non-zero)
    • angleInDegrees: number (optional, default 360) — Revolution angle in degrees (0 < angle ≤ 360)
  • Returns: BrepHandle — The revolved solid as a new brep
  • Throws: VALIDATION if axisDirection is zero-length, angleInDegrees is not in (0, 360], a coordinate is not finite, or the profile crosses the axis — the message names the offending segment (keep the whole profile on one side; points may touch the axis but not cross it); PRECONDITION_FAILED if the axis is skew to the profile's plane — neither in it nor parallel to it (move the revolution axis into the profile's plane); OPERATION_FAILED if the kernel cannot revolve the profile into a valid solid
ts
// A cylinder: revolve a 2m-wide, 3m-tall rectangle about the Y axis at its
// edge — the left edge sits ON the axis, which is legal.
const rect = await snaptrude.core.geom.create.profileFromLinePoints([
  await snaptrude.core.math.vec3.new(0, 0, 0),
  await snaptrude.core.math.vec3.new(2, 0, 0),
  await snaptrude.core.math.vec3.new(2, 3, 0),
  await snaptrude.core.math.vec3.new(0, 3, 0)
]);
const profile = await snaptrude.core.geom.create.contourFromProfile(rect);
const cylinder = await snaptrude.core.geom.create.brepFromRevolution(
  profile,
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 1, z: 0 }
);
await snaptrude.design.create.massFromBrep(cylinder, "rotunda");

// A dome: revolve a quarter-disc (pole ON the axis, arc out to the springing)
const pole = await snaptrude.core.math.vec3.new(0, 6, 0);
const springing = await snaptrude.core.math.vec3.new(6, 0, 0);
const origin = await snaptrude.core.math.vec3.new(0, 0, 0);
const axisPt = await snaptrude.core.math.vec3.new(0, 0, 1);
const shell = await snaptrude.core.geom.create.arc(pole, springing, origin, axisPt);
const base = await snaptrude.core.geom.create.line(springing, origin);
const rise = await snaptrude.core.geom.create.line(origin, pole);
const section = await snaptrude.core.geom.create.contourFromProfile(
  await snaptrude.core.geom.create.profileFromCurves([shell, base, rise])
);
const dome = await snaptrude.core.geom.create.brepFromRevolution(
  section,
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 1, z: 0 }
);
await snaptrude.design.create.massFromBrep(dome, "dome");

brepFromSphere(centre, radius) ​

Create a solid sphere B-rep from a centre and radius. Returns a new brep; commit it to the scene with design.create.massFromBrep. The result carries an exact spherical surface (not a faceted approximation) and a creation recipe — read the committed mass's parameters back with design.query.getParameters.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

  • Parameters:
    • centre: { x, y, z } — Sphere centre as plain components (raw Babylon units)
    • radius: number — Sphere radius (positive, finite, raw Babylon units)
  • Returns: BrepHandle — The new solid
  • Throws: VALIDATION if radius is not a positive finite number or a coordinate is not finite
ts
const brep = await snaptrude.core.geom.create.brepFromSphere({ x: 0, y: 5, z: 0 }, 5);
await snaptrude.core.geom.query.brep.getVolume(brep); // ≈ 523.6 — check before committing
const mass = await snaptrude.design.create.massFromBrep(brep, "Sphere");

brepFromCylinder(base, axis, radius, height) ​

Create a solid cylinder B-rep from its bottom-cap centre, axis direction, radius and height. Returns a new brep; commit with design.create.massFromBrep. The bottom cap sits at base and the solid extends height along axis — put base on the floor and use axis { x: 0, y: 1, z: 0 } for an upright column. Carries a creation recipe (radius, height).

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

  • Parameters:
    • base: { x, y, z } — Centre of the bottom cap as plain components
    • axis: { x, y, z } — Axis direction as plain components (non-zero; normalised by the host)
    • radius: number — Cylinder radius (positive, finite)
    • height: number — Cylinder height along axis (positive, finite)
  • Returns: BrepHandle — The new solid
  • Throws: VALIDATION if radius/height are not positive finite numbers or axis is zero-length
ts
const brep = await snaptrude.core.geom.create.brepFromCylinder(
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 1, z: 0 },
  1,
  4
);
const column = await snaptrude.design.create.massFromBrep(brep, "Column");

brepFromCone(base, axis, baseRadius, height, topRadius?) ​

Create a solid cone or frustum B-rep: bottom-cap centre, axis, base radius, height, and an optional top radius (default 0 — a full cone to a point; greater than 0 — a frustum / tapered column). Returns a new brep; commit with design.create.massFromBrep. Carries a creation recipe (radii + height).

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

  • Parameters:
    • base: { x, y, z } — Centre of the bottom cap as plain components
    • axis: { x, y, z } — Axis direction as plain components (non-zero; normalised by the host)
    • baseRadius: number — Radius at the base (positive, finite)
    • height: number — Height along axis (positive, finite)
    • topRadius: number (optional, default 0 = apex) — Radius at the top (≥ 0, and not equal to baseRadius)
  • Returns: BrepHandle — The new solid
  • Throws: VALIDATION if the radii or height are invalid, topRadius equals baseRadius (that is a cylinder — use brepFromCylinder), or axis is zero-length
ts
// A conical roof, apex up
const roof = await snaptrude.core.geom.create.brepFromCone(
  { x: 0, y: 9, z: 0 },
  { x: 0, y: 1, z: 0 },
  2,
  3
);

// A tapered column: 1m at the base, 0.6m at the top
const column = await snaptrude.core.geom.create.brepFromCone(
  { x: 0, y: 0, z: 0 },
  { x: 0, y: 1, z: 0 },
  1,
  6,
  0.6
);

brepFromTorus(centre, axis, majorRadius, minorRadius) ​

Create a solid torus (ring / donut) B-rep from its centre, plane normal, ring radius and tube radius. Returns a new brep; commit with design.create.massFromBrep. majorRadius is centre-to-tube-centre; minorRadius is the tube's own radius and must be strictly smaller. Both are radii, not diameters. Carries a creation recipe.

Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens of MB — expect a pause of seconds).

  • Parameters:
    • centre: { x, y, z } — Torus centre as plain components
    • axis: { x, y, z } — Plane normal of the ring as plain components (non-zero)
    • majorRadius: number — Ring radius, centre to tube centre (positive, finite)
    • minorRadius: number — Tube radius (positive, finite, and strictly less than majorRadius — a self-intersecting torus is rejected)
  • Returns: BrepHandle — The new solid
  • Throws: VALIDATION if a radius is invalid, minorRadius >= majorRadius, or axis is zero-length
ts
const ring = await snaptrude.core.geom.create.brepFromTorus(
  { x: 0, y: 6, z: 0 },
  { x: 0, y: 1, z: 0 },
  5,
  1
);
await snaptrude.core.geom.query.brep.getVolume(ring); // ≈ 2·π²·5·1² ≈ 98.7

Errors ​

Failed calls reject with a typed PluginError — see Error Handling. Conditions specific to this namespace:

CodeThrown byWhendetails
VALIDATIONprofileRectwidth or depth is not positivewidth, depth
VALIDATIONsplineFromPoints / profileFromSplinePointsFewer than 3 or more than 256 through-points, or two consecutive points coincide (for the closed profile, including last → first, or a self-intersecting loop)—
VALIDATIONsplineFromControlPointsFewer than degree + 1 control points (or more than 512), two consecutive control points coincide, or a degree other than 2 or 3—
VALIDATIONbrepFromFaces / brepFromMeshBad face set: degenerate/non-planar face, open shell, non-manifold edge, inconsistent winding; bad mesh index (brepFromMesh)—
VALIDATIONbrepFromExtrusionDegenerate extrusion (zero amount, zero direction, direction in the contour plane) or the contour cannot form a valid solidhandles
VALIDATIONbrepFromLoftCoincident contours, hole count mismatch, an arc/straight-edge pairing under edge-count auto-matching, a non-planar side face on an arc-bearing/holed pair (straight-edged hole-free pairs triangulate instead), holes or a non-planar section in a multi-section loft, invalid loft result, or — with compatibility: "strict" — mismatched section edge counts (or hole-pair edge counts; the error names the sections and counts)handles
OPERATION_FAILEDbrepFromLoftThe lofted segments could not be joined into a valid solid (twists are caught earlier as VALIDATION)handles
VALIDATIONbrepFromUnion / brepFromSubtraction / brepFromIntersectionThe boolean produced an empty solid, or disjoint solids (a brep holds exactly one solid)handles
OPERATION_FAILEDbrepFromUnion / brepFromSubtraction / brepFromIntersectionThe geometry kernel could not compute the boolean, or the result failed brep integrityhandles
VALIDATIONbrepFromFillet / brepFromChamferEmpty or duplicate-bearing edges (max 64), non-positive/non-finite radius or distance, an edge not on the brep, or an edge with no crease to round/cut — a seam of a closed face, or an edge between two patches of one smooth surface. Curved solids and curved or adjacent edges are accepted—
PRECONDITION_FAILEDbrepFromFillet / brepFromChamferAn edge could not be located on the kernel solid after healing (hint: select a different edge, or apply the operation before the step that curved the solid), or — brepFromFillet only — the kernel reported a faulty contour for the edge the message names (hint: reduce the radius or drop that edge). MakeChamfer has no such verdict, so a chamfer the kernel cannot build is OPERATION_FAILED—
OPERATION_FAILEDbrepFromFillet / brepFromChamferThe kernel or the translation failed, or the result is non-manifold, splits into several solids, or contains a surface Snaptrude cannot represent—
VALIDATIONbrepFromOffsetNon-finite distance, a distance too small to offset anything, or an inward distance that consumes the solid entirely—
VALIDATIONbrepFromShellEmpty or duplicate-bearing openFaces (max 32), a face not on the brep, non-positive/non-finite thickness, or a thickness too large (must be smaller than half the solid's smallest span)—
PRECONDITION_FAILEDbrepFromOffset / brepFromShellNot raised by these two today. An untouched face — a kept face that could not be thickened (shell) or an unmoved face (offset) — is logged, not refused, so such a failure arrives as OPERATION_FAILED (hint: reduce the thickness/distance, or open a larger face). Only design.update.parameters refuses with this code for a shell/offset, when a recorded selector no longer resolves on the rebuilt base—
OPERATION_FAILEDbrepFromOffset / brepFromShellThe kernel or the translation failed, or the result is non-manifold, splits into several solids, or contains a surface Snaptrude cannot represent (an offset free-form NURBS surface may have no exact representation — the model is left unchanged)—
VALIDATIONbrepsFromSplitZero-length plane normal, or non-finite origin/normal components—
OPERATION_FAILEDbrepsFromSplitThe kernel could not split the solidhandles
VALIDATIONbrepFromSweepProfile has holes, path shorter than 2 points, coincident consecutive points, non-finite coordinates, the profile plane contains the first path segment's direction, a bad closed path (non-planar, doubles back, ❤️ corners, arc profile, oversized profile), a bad taper (varying scale on a closed/bent path, arc profile, out-of-range or mis-sized scales), or a { bevel } that does not fit a path leg or whose corner nearly doubles back (the error names the corner)handles
PRECONDITION_FAILEDbrepFromSweeptransition: "round" on a closed path — the closed lane never touches the kernel. hint: sweep the open runs with rounded corners and union them, or use "miter" / { bevel } on the ring—
OPERATION_FAILEDbrepFromSweepThe kernel could not sweep the profile into a valid solidhandles
VALIDATIONbrepFromRevolutionZero-length axis, angleInDegrees outside (0, 360], non-finite components, or a profile that crosses the axis (the message names the offending segment — points may touch the axis but not cross it)handles
OPERATION_FAILEDbrepFromRevolutionThe kernel could not revolve the profile into a valid solidhandles
VALIDATIONbrepFromSphere / brepFromCylinder / brepFromCone / brepFromTorusA radius or height is not a positive finite number, a coordinate is not finite, axis is zero-length, topRadius === baseRadius (that is a cylinder), or minorRadius >= majorRadius (a self-intersecting torus)—
HANDLE_INVALIDall methodsA point, curve, profile, contour, face, edge, or brep handle cannot be resolved—