tscad GitHub

Design checks and model tests

Two modules help you trust a parametric model across all its parameters:

Testing a model

// bracket.test.tsx
import { describe, expect, test } from "bun:test"
import { evaluateModel, paramCombos } from "tscad/testing" // registers the matchers
import * as bracket from "./bracket"

describe("bracket", () => {
  test("defaults", async () => {
    const r = await evaluateModel(bracket)
    expect(r).toEvaluate()
    expect(r).toFitIn([60, 40, 45])
    expect(r).toBePrintable({ minWall: 0.8, bed: [220, 220, 250] })
  }, 60_000)

  test.each(paramCombos(bracket.params, { samples: 3 }))("evaluates for %o", async (p) => {
    const r = await evaluateModel(bracket, p)
    expect(r).toEvaluate()
    expect(r.size()[0]).toBeCloseTo(p.width, 3)
  }, 60_000)
})

Run model tests with bun test ./examples/ (plain bun test only runs tests/; see bunfig.toml). A full example is in examples/tests/bracket.spec.tsx. Kernel evaluations take a second or more, so give model tests a timeout such as 60_000.

evaluateModel(model, params?, { kernel?, tolerance? })

model can be an imported model module (import * as m from "./m"), a component, or a JSX element. The function compiles the model, evaluates it, and fetches every part's mesh. It never throws for modelling errors; check them with toEvaluate() or r.ok. The result (ModelResult) has:

ok, parts, part(name), warnings, values, documentthe evaluation
volume(part?), mass(part?)mm³; grams from material/density
bbox(part?), size(part?)bounds and [x, y, z] size, mm
mesh(part)the display mesh
printability(opts?, part?), watertight(part?)Map<part, result> (sync)
interference(opts?)Promise<InterferenceResult> (computed once)
describeErrors()failures with their TSX locations

Matchers

MatcherPasses when
toEvaluate()the model compiled and every part evaluated
toHaveVolume(v, tol = 1e-3, part?)the volume is v mm³ (a tol < 1 is relative, ≥ 1 is in mm³)
toFitIn([x, y, z])the bounds fit inside the box
await toHaveNoInterference(minVolume?)no two parts share volume. It is async, so await expect(r).toHaveNoInterference()
await toMatchRenderSnapshot(name or options)the model renders like its stored snapshot (see Render snapshots)
toBePrintable(opts)the overhang share of the surface is ≤ maxOverhangFraction (default 5 %), the thin-wall share is ≤ maxThinFraction (0.5 %), the part fits bed, and the mesh is watertight

toBePrintable takes the same options as checkPrintability (below), plus maxOverhangFraction, maxThinFraction and watertight.

Render snapshots

A render snapshot catches changes that numbers miss: a fillet that moved, a hole that vanished, a part that rotated. toMatchRenderSnapshot renders the model with the headless renderer (tscad/render, no GPU or browser) and compares it with a PNG stored next to your tests.

import { expect, test } from "bun:test"
import { evaluateModel } from "tscad/testing"
import * as bracket from "./bracket"

test("bracket looks the same", async () => {
  const r = await evaluateModel(bracket)
  await expect(r).toMatchRenderSnapshot({ name: "bracket", dir: "tests/__snapshots__" })
  await expect(r).toMatchRenderSnapshot({ name: "bracket", view: "top", dir: "tests/__snapshots__" })
}, 60_000)

toMatchRenderSnapshot("name") is short for { name }, which stores the PNG in __snapshots__ under the current directory. The options:

Option
namefile name, <name>.<view>.png
dirwhere the PNGs live, default __snapshots__
view"iso" (default), "top", "front", "right", "back", "left", "bottom"
width, heightimage size, default 256 × 192
tolerance, maxDiffRatiosee above
quantizecolour channels are rounded to multiples of this before saving and comparing, which halves the file size (default 8; 1 keeps full precision)
params, evaluate, renderparams and kernel options when you pass a module, and extra renderer options

The matcher takes a ModelResult, a model module, a component or an element. The same check is available as a function, await matchRenderSnapshot(model, options). It returns { pass, written, path, diffPath, diffRatio, message } and never throws for a mismatch. diffImages(a, b, tolerance) compares two images yourself.

The renderer is plain TypeScript, so images are the same on every machine. The native and wasm kernels produce the same meshes, so their renders match pixel for pixel. tscad's own tests keep a snapshot of every example in tests/__snapshots__.

Parameter sweeps

paramCombos(schema, options) returns parameter sets to test with. It takes every combination of the boolean and choice params, and for each one it samples the numeric params (with both min and max) across their range. Values snap to step and ints are rounded. Text and colours stay at their defaults.

Option
method"lhs" (latin hypercube, default), "random", "corners" (every min/max), "grid"
samplesnumeric samples per discrete combination (or levels per number for grid), default 4
seedreproducible sampling, default 1
defaultsinclude the all-defaults set first, default true
maxcap on the number of sets (sampled evenly), default 64
only, fixedvary only these params; pin some values

forEachParamCombo(schema, options, fn) runs fn(values, i) on each set in turn and returns how many it ran.

Checks

import { checkPrintability, checkInterference, checkWatertight } from "tscad/check"

const mesh = await kernel.mesh("bracket")
const p = checkPrintability(mesh, { buildDir: [0, 0, 1], maxOverhangDeg: 45, minWall: 0.8, bed: [256, 256, 256] })
p.overhang.area          // mm² steeper than 45° from vertical (not counting faces on the bed)
p.overhang.faces         // grouped by B-rep face: { faceId, area, maxAngle }
p.thickness!.min         // thinnest wall found, mm
p.bed                    // { fits, rotated }
p.warnings               // human-readable problems

Viewer heatmaps

Everything is per triangle of the mesh you pass in, in the order of mesh.indices. This matches MeshArrays from the kernel.