@nfcard/surface-kernel
v0.9.0
Published
Placement and manufacturability rules for NFCard printed surfaces.
Readme
@nfcard/surface-kernel
The NFCard surface engine, compiled to WebAssembly.
One kernel decides where an element may sit on a printed surface and whether the result can
actually be manufactured. The browser calls this package; nfcard-cardgen calls the same Rust
through a Python wheel. Two bindings, one implementation — which is the entire point: a rule
that lives in two languages is a rule that disagrees with itself eventually.
Proprietary. Published so NFCard's own builds can install it; not licensed for other use.
Install
pnpm add @nfcard/[email protected]Pin exactly, without a caret. This is a geometry contract, and a silent minor bump changes what the configurator will let a customer buy.
Use
Built with wasm-pack --target web, so the module exports an init you must await once before
anything else. Pass the .wasm URL explicitly rather than relying on import.meta.url
resolution, which is fragile under bundler pre-bundling:
import init, { SurfaceSession, kernelVersion } from '@nfcard/surface-kernel'
import wasmUrl from '@nfcard/surface-kernel/surface_wasm_bg.wasm?url'
await init({ module_or_path: wasmUrl })
const session = new SurfaceSession(specJson) // decode ONCE
const report = session.validate() // every DFM verdict + forbidden bandsSurfaceSession is a session object rather than a set of free functions because constrain is
called on every pointer move of a drag. Decoding the spec per frame would make the drag feel
heavy, so the spec is decoded once and the hot call takes bare numbers:
const { transform, blocked, applied } = session.constrain(
'front', 'logo-1', xMm, yMm, rotationDeg, scale,
)blocked means no clamp rescues this position — the drop must be refused, not relocated.
What it decides, and what it does not
It owns the manufacturing envelope: what the plastic, the nozzle and the NFC antenna will tolerate. It does not own interaction feel — grid snap, rotation steps and the nearest-free search stay in TypeScript, where they get tuned constantly and recompiling Rust for a feel tweak would be the wrong locality.
The split against the Python side is sharper than it looks:
The kernel refuses on authored intent — 2D placement and parameters, before any mesh exists. Python refuses on realized artifacts — meshes after CSG, including the file actually written.
Neither subsumes the other. A cutter that seals an internal cavity is invisible from 2D; a chip 3 mm from its neighbour is invisible from a finished mesh.
v0 status
Implemented: decode, fromCardDesign, catalog outlines and body placement, the DFM rule set,
faceFrames, constrain, and the forbidden regions the configurator greys out mid-drag.
Not implemented: layout and preview_mesh. Both need realised solids rather than an outline.
They throw with a reason rather than returning a plausible wrong answer, so a caller can never
mistake a stub for a result.
Every refusal carries the provenance of the constant behind it — measured, asserted,
inherited or derived — and report.allConstantsMeasured tells you whether anything in this
verdict rests on a number nobody has put on a bench yet. A hard refusal backed by a guess is
worth knowing about.
kernelVersion() matches the Python wheel and is stamped into the 3MF that cardgen produces, so
a printed file and a browser preview can be traced to the same build.
