Solid backends: CSG and SDF

bosl2 can realize a solid two different ways, and the choice is a switchable backend:

  • CSG (the default) — exact constructive solid geometry via PythonSCAD’s native primitives. Booleans are exact, the attachment/anchoring system is available, and shapes carry their BOSL2 metadata. This is today’s Bosl2Solid.

  • SDF — an F-Rep / signed-distance-field engine built on libfive (the merged pysolidfive code). Shapes are implicit surfaces, so they round and blend smoothly and mesh at any resolution, at the cost of the CSG-only attachment features. Its solids are PyShape objects.

Both backends expose the same shared constructors, so the same code builds either one.

The bosl2.solid facade

bosl2.solid is the backend-neutral entry point. Each constructor dispatches to whichever backend is active and returns a common Solid — a Bosl2Solid on CSG, a PyShape on SDF:

from bosl2.solid import sphere, use_backend

a = sphere(radius=10)          # CSG (default)  -> Bosl2Solid
with use_backend("sdf"):
    b = sphere(radius=10)      # libfive SDF     -> PyShape

The shared 3-D surface both backends provide:

  • cube

  • cuboid

  • cyl

  • cylinder

  • octahedron

  • onion

  • pie_slice

  • prismoid

  • rect_tube

  • regular_prism

  • sphere

  • spheroid

  • teardrop

  • torus

  • tube

  • wedge

  • xcyl

  • ycyl

  • zcyl

plus the n-ary booleans union, difference and intersection. The backend-specific modules (bosl2.shapes3d, bosl2._sdf) stay directly importable for anything not yet unified in the facade.

Selecting a backend

Call

Effect

current_backend()

The backend active in this context ("csg" by default).

use_backend("sdf")

A context manager making its argument the active backend for the with block. Nestable and thread/async-safe (it is a contextvars.ContextVar).

set_default_backend("sdf")

Change the process-wide default (outside any use_backend block).

All three are re-exported from bosl2.solid for convenience.

Combining and converting between backends

A boolean or transform requires its operands to share the active backend. Combining solids from two different backends raises CrossBackendError rather than producing nonsense:

from bosl2.solid import cube, sphere, use_backend

c = cube(10)                       # csg
with use_backend("sdf"):
    s = sphere(radius=6)           # sdf
c | s                              # raises CrossBackendError

Convert first with the bridge methods:

Method

Result

pyshape.to_csg()

Mesh the SDF field and wrap it as a Bosl2Solid — the supported SDF → CSG bridge.

pyshape.to_sdf() / bosl2solid.to_csg()

Identity (already on that backend).

bosl2solid.to_sdf()

Raises UnsupportedByBackend — an exact CSG solid has no faithful distance field. Rebuild it under use_backend("sdf") instead.

So the CSG → SDF direction is deliberately not automatic: build with the SDF backend from the start when you want an implicit surface.

Capability map: what each backend can do

Most of the surface is shared, but a few features belong to one backend only. Calling one on the wrong backend raises UnsupportedByBackend (with a hint) instead of a confusing AttributeError — and, on the SDF side, instead of meshing via libfive just to fail:

Backend

Exclusive features

Why

CSG only

attach, anchor_point, reanchor, position, align, reorient, orient, edge_mask, edge_profile, edge_profile_asym, corner_profile, face_profile

BOSL2’s attachment / anchoring system has no signed-distance equivalent.

CSG only

projection, fill, and all 2-D geometry (Bosl2Shape2D, Path.polygon(), Path.hull(), rotate_extrude, a 2-D stroke())

Only the CSG backend has a 2-D shape object; an SDF is a field over 3-space, with no 2-D shadow to project and no outline to fill. See below.

SDF only

round, chamfer (as solid methods)

Implicit-surface edge treatments. On CSG use the rounding= / chamfer= parameters of cuboid() / cyl() instead.

Query support directly with supports(backend, feature):

from bosl2._backend import supports

supports("csg", "attach")   # True
supports("sdf", "attach")   # False
supports("sdf", "round")    # True
supports("csg", "sphere")   # True  -- shared surface

2-D on the two backends

2-D geometry is a CSG-only notion: Bosl2Shape2D and every bosl2.shapes2d constructor build exact 2-D shapes, and stay on the CSG backend even inside a use_backend("sdf") block (they do not silently change meaning). A path, on the other hand, is just points and so is backend-neutral — and the operations that take a path to a 3-D solid dispatch on the active backend:

Call

CSG

SDF

Path.linear_extrude(), Region.linear_extrude()

native linear_extrudeBosl2Solid; takes center/twist/scale/slices

polygon_prismPyShape; takes center plus rounding_top/rounding_bottom/res, and rejects the profile-shearing options

Bosl2Solid.hull() / PyShape.hull()

exact native hull()

polyhedral hull of the children’s support points

stroke() of a 3-D path

a tube of Bosl2Solid cylinders/spheres

the same tube as one distance field

Bosl2Solid.projection(), Path.polygon()/.fill()/.hull()/.rotate_extrude(), 2-D stroke()

Bosl2Shape2D / Bosl2Solid

UnsupportedByBackend

So the same source builds on either backend as long as it goes path → solid:

outline = Path([[0, 0], [40, 0], [40, 25], [0, 25]]).round_corners(radius=5)
plate = outline.linear_extrude(height=4)              # -> Bosl2Solid
with use_backend("sdf"):
    field = outline.linear_extrude(height=4)          # -> PyShape

Sweeping a profile along a path (SDF)

The SDF backend can sweep a 2-D profile (convex or concave) along a 3-D path directly as a distance field (bosl2._sdf.shapes3d.path_sweep / bezier_sweep). The profile is placed in a rotation-minimizing frame at each path sample and unioned; the cross-section itself is evaluated with the convex-deficiency decomposition (the same one polygon_prism uses over the convex-only polygon_extrude), so concave outlines are handled correctly. The result is a true SDF — it can be .round()/.chamfer()``ed, meshed at any resolution, or bridged to CSG with ``.to_csg(). Bezier generation stays bosl2’s canonical Bezier; the sweep just consumes the sampled curve:

import math, numpy as np
from bosl2._sdf.shapes3d import bezier_sweep

circle = [[2 * math.cos(t), 2 * math.sin(t)] for t in np.linspace(0, 2 * math.pi, 24, endpoint=False)]
tube = bezier_sweep(circle, [[0, 0, 0], [0, 0, 20], [25, 12, 15], [30, 4, 6]])

This is distinct from the CSG sweeps (bosl2.beziers.Bezier.sweep(), skin, offset_sweep), which build a VNF/polyhedron mesh rather than a distance field. Denser paths give a smoother lateral surface; the ends cap perpendicular to the path.