Embedded JS console · NASSCAD 4.7.0 MEDUSA

NassScript — the whole surface

Every global the ⚡ Script console can reach, read from the shipped source and then confirmed by enumerating the live global scope in a running instance. Signatures, semantics, ready-to-paste scripts, and the places where the API will bite you.

Global functions
576
Public (no _)
189
CSG engine
MEDUSA
B-Rep kernel
OCCT WASM
Renderer
three.js
Access gate
none

No entry matches.

01Execution model

Whatever you paste into the modal is handed to new Function('return (async()=>{…})()') and run on the main thread. Three consequences account for most surprises.

Scope

Global read, local write

  • Every NASSCAD global is visible
  • Your const/let die with the run
  • To persist: window.x = …
Async

Top-level await works

  • The body is already an async IIFE
  • 26 shipped functions are async
  • A missed await interleaves steps
Thread

No preemption

  • Stop is cooperative, not a kill switch
  • Call scriptCheckStop() per iteration
  • Rendering freezes inside a sync block
scriptLog(...args)
Appends a line to the output pane during the run instead of waiting for the return value. Non-string arguments go through a circular-safe JSON.stringify, so handing it a three.js mesh no longer breaks the display.
scriptCheckStop()
Throws ⏹ Stopped by user if Stop was pressed. Nothing calls it for you — a loop without it cannot be interrupted, because synchronous JS never yields the main thread.
stopScript()
Raises the stop flag. This is what the Stop button calls; from a script it is also how you build a deadline (setTimeout(stopScript, 30000)).
runScript() · showScriptEditor() · hideScriptEditor() · clearScriptEditor()
The modal itself. Opening it requires no password.
asyncDOM

Return value, output, history

Whatever the script returns is printed last, prefixed ; with no return you get ✓ OK. A thrown error prints ✗ message in red and is mirrored to the log at ERROR level. The last 20 scripts live in sessionStorage under nasscad_script_hist — they survive a reload, not a tab close.

02Scope & reachability

NASSCAD's globals live in two different places, and the difference is visible from a script. This is verified behaviour, not theory.

// function declarations land on the global OBJECT
typeof window.addPrimitive   // "function"

// top-level const / let land in the global LEXICAL environment only
typeof window.objs           // "undefined"  ← not a bug
typeof objs                  // "object"     ← bare name works
typeof window.NASSCAD_MATERIALS // "undefined"
NASSCAD_MATERIALS.length      // 12

Both kinds are reachable from NassScript, because the script body is evaluated in global scope. The distinction matters in exactly two situations: feature-detecting with typeof window.X (use the bare name instead), and assigning a value you want to survive the run (window.myCache = … works; a bare const does not).

What is not reachable

The 2D sketcher is an IIFE-isolated module: 102 of its functions never touch the global scope. It publishes exactly three names — openSketcher, closeSketcher, and the SK object described in its own section. Likewise every _qf* helper of the fillet module and every _p21* STEP parser helper is a plain global, while the OCCT kernel handle itself is created on demand and only handed out by _occtLoad().

Naming convention

A leading underscore means internal. Those functions work, several are genuinely the best tool for a scripting job, and this page documents the useful ones — but they are not a contract. Anything without an underscore is part of the app's own surface and far less likely to move.

03Data model

A NASSCAD object is a small record wrapping a three.js mesh. Every other API works on this shape.

// one entry of objs[]
{
  id:        42,                  // unique integer — the key used by _csgTree
  name:      'Cylinder_42',        // shown in the object list
  type:      'cylinder',           // source primitive, or 'csg' after a boolean/generator
  mesh:      THREE.Mesh,          // geometry + material + transform
  color:     '#5b8fc9',            // list swatch (independent of an applied material)
  isHole:    false,               // true ⇒ subtracted by the next boolean
  // situational:
  genType:   'gear',                // gear|screw|nut|pipe|lathe|cylind|cubic|tore|arcsphere|circtext|sketch
  genParams: {…},                 // the generator's exact inputs — replayable
  tubeRo, tubeRi,                 // outer / inner radius of a tube
  sphereRes,                      // sphere segment count
  matId:     'inox',                // applied NASSCAD material
  isManifold,                     // false ⇒ CSG will warn
  isOcctResult,                   // came out of a fillet/chamfer
  _poolSlot                       // GeometryPool slot, when pooled
}

The CSG construction tree

_csgTree is a Map keyed by object id. Each node stores the operation, the result centre of gravity, and a full snapshot of every source — enough to rebuild them without the original objects, which is what makes Re-run and Explode non-destructive.

_csgTree.get(id) = {
  op: 'union' | 'subtract' | 'intersect',
  cg: [x, y, z],
  children: [{
    id, name, type, color, isHole, tubeRo, tubeRi, genType, genParams,
    p: [x,y,z], r: [rx,ry,rz], s: [sx,sy,sz],   // position / rotation / scale
    opacity, wireframe,
    _csgTree: {…} | null,                     // nested node — the tree is recursive
    _csgGeo:  {…}                                // serialised geometry, only when not rebuildable
  }]
}
objs : Array
Every object in the scene. ReassignabledelSel() itself does objs = objs.filter(…). Never hold a reference to the array across an operation; re-read objs.
selObjs : Array
The selection, in click order. The last element is the active object — it is the only one setTr, setDim and renameObj can see.
objCnt : number
Id counter, bumped on every creation. Never wind it back.
isHoleMode : boolean
Current creation mode. While true, new primitives are born as holes — red, 45 % opaque, and subtracted by the next boolean.
scene · cam · ren · ray · mouse · THREE
Scene graph, camera, WebGL renderer, raycaster, normalised pointer, and three.js itself — so Box3, Vector3, Matrix4, BufferGeometry and the rest are all available.
PS = 20 · GS = 220 · COL[10] · NASSCAD_VERSION
Base primitive size in mm, grid half-extent, the cycling colour palette, the version string. PS is the reference for every scale computation: a primitive at scale 1 measures 20 mm on each axis.
_newPrimRes = 32 · RES_MAX = 256 · RES_EXP_CAP = 512 · SEG_VIEW
Creation resolution in segments, its display and export ceilings, and the per-shape segment table. _segViewLive() derives the object actually passed to makeGeo.
snap · snapSz · SNAP_SIZES = [1, 0.5, 0.1, 0.01, 0.001]
Magnetic snapping state and the ladder of steps in millimetres.
tool · alignMode · cotEnabled · cotVisible · _workerReady · _csgBusy
Interaction state: active tool, alignment mode, dimension overlay enabled/shown, last known MEDUSA reachability, and whether a boolean is currently running.

04Creating geometry

Eleven mesh primitives, plus a documented door for any geometry a script computes itself.

addPrimitive(type)
Builds a 20 mm primitive, rests it on the grid, spirals outward to a free spot, then selects it alone. Any unknown type silently falls back to a cube.
undo
typeShapeTriangles at 32 segNotes
cubeBox 20³12
sphereUV sphere Ø202 520stores sphereRes; smooth normals
cylinderØ20 × 20272centred on its own origin
coneØ20 × 20204
pyramidSquare base, apex6flat-shaded
roofGable prism8ridge height 10
roofarcBarrel roof192segments ×1.5
halfsphereDome1 152segments ×1.5
tubeHollow cylinder384sets tubeRo = 10, tubeRi = 5
hollowboxOpen box32outer / inner wall via its dialog
wedgeRamp8
makeGeo(type, S, res) → BufferGeometry
The raw geometry factory, with no scene involvement. S is the edge size in mm; res is the segment table — pass _segViewLive() to match the current setting.
makeGeoHD(obj) · makeGeoCSG(obj)
Rebuild an object's geometry at export quality / CSG quality. This is why a sphere displayed at 32 segments can leave as a 256-segment STL. Objects that cannot be rebuilt (imported meshes, boolean results) return a clone of what is on screen.
_finalizeGenToScene({label, geo, name, genType, genParams, hideDialogFn, shininess, specular}) → obj
The script's way into the scene. Takes any THREE.BufferGeometry and gives it a material, a free position, an id, a list entry and an undo step, then returns the created object. name may be a string or a function of the new id. Pass hideDialogFn: ()=>{} outside a dialog.
undo
postProcessCSGGeo(geo, creaseAngleDeg) → Promise<BufferGeometry>
The finishing pass booleans use: vertex welding plus crease-angle normals (30° default). Worth running over geometry you generated yourself before handing it to _finalizeGenToScene.
async
showTubeDialog(o) · applyTubeDims() · showHollowBoxDialog(o) · applyHollowBoxDims()
Parametric dialogs for the two primitives that carry their own dimensions. Passing an existing object switches them to edit mode.
DOM
showTextDialog() · create3DText()
Extruded 3D text from the bundled typefaces. create3DText() reads the dialog fields, so the dialog has to be open and filled.
DOM
dupSel() · copyObjs() · pasteObjs()
Duplicate in place (offset by PS on X, named _Cp01, _Cp02…) or through an internal clipboard. CSG geometry is cloned independently, and genParams plus CSG history are carried over.
selectionundo
setSphereResLive(v) · toggleAdaptiveTess()
Sets the creation resolution (clamped 8…RES_MAX) — with a selection it retessellates it live, without one it changes the default for new primitives. Adaptive tessellation scales segments with object size.

05Selection

Selection drives most of the rest of the API. From a script, assigning it directly beats chaining calls.

selObj(obj, add)
add=false replaces the selection; add=true appends — or removes if the object was already selected. selObj(null, false) clears.
selectAllObjs()
Selects the whole scene and refreshes the panels.
setHoleMode(bool)
With no selection it flips the creation mode. With a selection it converts those objects, remembering a multi-colour palette so switching back restores it.
undo
delSel()
Deletes the selection, disposes geometries and materials, drops the _csgTree entry, and reassigns objs.
selectionundo
updProps() · updOList(selOnly) · updCSG() · updStats()
The four refreshers: properties panel, object list (also rebuilds the id/mesh lookup maps and the bounding-box cache), CSG button state, vertex/face/object counters. After assigning selObjs yourself, call at least the first three.
toggleAlign() · doAlign(mode) · applyAlignAnchor(mode, groupBox) · hideAlignDots()
Group alignment against a shared anchor; needs two or more selected objects.
selectionundo
// selection by predicate — the single most useful pattern
selObjs = objs.filter(o => o.genType === 'gear' || /^Cylinder_/.test(o.name));
updProps(); updOList(true); updCSG();
scriptLog(selObjs.length + ' object(s) selected');

06Transforms

Two routes: the panel functions, or the mesh directly. In a script the second is almost always right.

setTr('position'|'rotation'|'scale', 'x'|'y'|'z', value)
Acts on the last selected object only. Rotation in degrees. Position is converted from the display unit — in inches, setTr('position','x',1) moves 25.4 mm. And on Y the value addresses the bottom of the bounding box, not the mesh origin.
selectionundo
setDim('x'|'y'|'z', value)
Forces an overall dimension on the active object. The axis is a world axis, remapped to the matching local axis for rotated parts — exact for 90° multiples, best effort otherwise. On Y the object is then reseated on the ground. Also display-unit sensitive.
selectionundo
scaleSelUniform(factor)
Multiplicative scale across the whole selection about the group's median pivot, so positions spread proportionally and an assembly stays coherent. scaleSelUniform(1) is an absolute reset, not a no-op.
selectionundo
recenterCG()
Recentres geometry on its true volumetric centre of gravity without moving the object on screen.
selectionundo
dropToGround()
Rests the selection on the grid plane — which is not necessarily Y=0, since the grid can be dragged.
selectionundo
computeCenterOfGravity(geo) → Vector3
Volumetric centroid by signed-tetrahedron summation, in the geometry's own space. Falls back to the bounding-box centre when the volume is degenerate.
_invalidateBbox(mesh)
Call it after moving a mesh by hand. Otherwise picking, hover and the dimension overlay keep using the stale bounding box.
toggleCotation() · applyCotDim(axis) · toggleMeasure()
The 3D dimension overlay with editable fields, and the point-to-point measuring tool.
DOM
The habit to build

In a script prefer the mesh: o.mesh.position.set(x,y,z), o.mesh.rotation.y = a, o.mesh.scale.set(…) — always millimetres, always the object you mean, followed by _invalidateBbox(o.mesh). setTr and setDim are panel functions: one object, and they read the display unit.

07Appearance & materials

setCol('#rrggbb')
Applies one colour to the whole selection. Destructive on a multi-colour part: per-face colours read from a STEP file are collapsed into a single material and lost.
selectionundo
setOpa(0..1)
Opacity across the selection; flips the material to transparent below 1.
selectionundo
applyNasscadMaterial(id)
Full material — colour, family-derived specular, shininess, opacity — and records o.matId. The step triplet of each entry is what gets written into a STEP file.
selectionundo
NASSCAD_MATERIALS[12] · NASSCAD_MATERIALS_BY_ID · nasscadSpecularOf(m) · nasscadHexString(hex) · nasscadMaterialSelfCheck() · nasscadBuildMaterialUI()
The palette itself and its helpers. Entries are {id, name, step:[r,g,b], hex, family, shininess, opacity}; specular is derived from the family rather than stored.
idMaterialHexFamilyShininessOpacity
aluAluminiumBABABAmetal601
inoxStainless steelA6A6ABmetal901
acierPlain steel737373metal251
laitonBrassD4AD36metal851
cuivreCopperB87333metal801
orGoldFFD600metal1101
titaneTitanium9999A6metal551
abs-noirBlack plastic1A1A1Aplastic301
abs-blancWhite plasticF2F2F2plastic351
caoutchoucRubber333333rubber61
carboneCarbon / CFRP262626composite451
verreGlassB2D9E6glass1000.35
setWire() · toggleXRay() · toggleXRayObj(id) · toggleFeatureEdges() · setLightIntensity(v)
Wireframe, global and per-object X-Ray, relief edges, key-light intensity. Global X-Ray wins over the per-object flag.
async
renameObj(name)
Renames the active object and propagates the name into parent _csgTree nodes. For bulk renaming, writing o.name then calling updOList() is more direct.
selectionundo
toggleTheme() · toggleGrid() · toggleNavCube() · buildGrid() · buildAxes() · buildNavCube() · updColors()
Viewport furniture. toggleTheme() swaps the app's light/dark theme and rebuilds the grid and axis colours.

08Boolean CSG

All three booleans go to MEDUSA over local HTTP. There is no in-browser fallback: with the engine stopped, the operation fails loudly instead of degrading.

doCSG('union'|'subtract'|'intersect')
Operates on selObjs, two objects minimum. If the selection mixes solids and holes the operation is forced to solids − holes, whatever you asked for, with solids ordered first. Unions of non-overlapping objects bypass the engine entirely, and a union of union-trees is flattened to its leaves before being sent.
asyncselectionMEDUSAundo
_medusaProbe(timeoutMs) → Promise<bool> · _workerReady · _medusaRequire()
Probes the engine and updates the badge; _workerReady caches the last answer. Check it before a batch rather than after the first failure. _medusaRequire() throws a descriptive error when the engine is missing.
async
_medusaCSG(op, meshes, hasMix, solidsIdx, holesIdx, label) · _medusaPost(path, body, wdogMs, label)
The raw engine calls under doCSG: POST /csg for a flat operation, POST /csgtree for a whole tree. Useful when you want the result mesh without the scene bookkeeping.
asyncMEDUSA
rerunCSG() · deepRerunCSG()
Replays a node after one of its sources changed — one level, or recursively down the whole tree.
asyncselectionMEDUSA
explodeCSG() · explodeDeepCSG() · showCSGTree()
Takes a boolean result back apart into its sources — one level, or down to the leaves — and displays the tree. Requires a _csgTree entry, which imported objects never have.
selectionundo
setCsgQuality(32|64|128|256|512) · setExpQuality(n)
Segment counts used when primitives are rebuilt for a boolean or an export. 512 segments is roughly 524 000 triangles per sphere — budget accordingly.
setWatchdog(s) · setWorkerCap(n) · enableAutoWdog() · enableAutoWorkers()
Operation timeout and worker ceiling, manual or automatic. Setting a value by hand turns the matching auto mode off.
Before looping over booleans

Every doCSG is a local request plus a retessellation. Order matters: union all the holes into one object and subtract once, rather than subtracting N times. It is faster and it leaves a single node in the tree instead of N nested ones.

09Fillet & chamfer — OpenCASCADE

The one place NASSCAD handles real B-Rep locally: the mesh is sewn into a solid, coplanar facets are merged back into faces, and BRepFilletAPI works on that. Entirely in the browser — no MEDUSA involved.

toggleQuickFillet()
Enters Quick Fillet: edge-chain scanning, mouse picking, radius, then round or chamfer on picked edges or all of them. Interactive only — there is no scriptable equivalent.
OCCTDOM
_occtLoad() → Promise<oc>
Loads and instantiates the kernel — 65 MB, once per session, cached for reinstantiation — and hands back the oc handle, i.e. the full OCCT binding: BRepBuilderAPI_*, BRepFilletAPI_MakeFillet, TopExp_Explorer_2, BRepGProp, ShapeUpgrade_UnifySameDomain_2, BRepCheck_Analyzer
OCCT
_occtVolume(oc, shape) · _occtIsValid(oc, shape) · _occtEdgeLen(oc, edge) · _occtDrop(...x)
Volume via BRepGProp, topological validity via BRepCheck_Analyzer, chord length of an edge, and the disposer that calls .delete() on anything you pass it.
_occtResetKernel(why) · _occtIsFatal(msg)
A WASM abort leaves the module unusable. These detect the fatal signatures — out of memory, unreachable, table index out of bounds — and discard the kernel so the next call reinstantiates in about two seconds instead of forcing a reload.
What "B-Rep" means here

The solid handed to OCCT is built from the mesh: one triangle becomes one planar face, then coplanar faces are merged. A cylinder stays N planar strips — it never becomes an analytic cylindrical surface. The fillets and the recovered edges are real; the original surfaces of a part that was never a B-Rep are not recoverable.

Every OCCT instance a script creates must be released by hand with .delete(). Embind has no garbage collector and the WASM heap tops out at 2 GB — this is the cause of "after N fillets nothing works until I reload".

10Sketch.Gen — window.SK

The 2D sketcher is an isolated module, and it is the only part of NASSCAD that ships a purpose-built scripting object. Its geometry engine stays private; these eighteen entries are the whole public surface.

openSketcher(editObj) · closeSketcher() · SK.close()
Opens the sketch overlay. Passing an object whose genType is 'sketch' reloads its stored entities for editing.
DOM
SK.getEntities() → Array
Deep copy of the current sketch entities — lines, arcs, circles, béziers with their ids and coordinates. Read-only by design; the code calls it out as the console-friendly entry point.
SK.setTool(name)
Switches the active drawing tool, exactly as the toolbar does.
SK.selectEntity(id) · SK.updateProp(id, key, val) · SK.deleteSelected() · SK.clearAll(force)
Select, edit one property of one entity by id, delete the selection, or wipe the sketch. This is the scriptable path to parametric 2D: read with getEntities(), write back coordinate by coordinate.
SK.applyConstraint(type)
Applies a constraint to the current selection.
SK.extrude()
Extrudes the closed contour into a watertight solid and pushes it into the scene as an object with genType: 'sketch', its entities stored in genParams.
undo
SK.undo() · SK.redo() · SK.canUndo() · SK.canRedo()
The sketch's own history — full snapshots, 120 deep, separate from the 3D undo journal. The can* pair returns the stack depth, so they double as counters.
SK.fitView() · SK.toggleGrid() · SK.toggleSnap(key, el) · SK.toggleTheme()
Sketch viewport controls. These are the sketcher's own, unrelated to the 3D view functions with the same names.
SK.exportNass()
Exports the sketch in NASSCAD's own 2D format.

11Generators

Nine generators read their inputs from their dialog fields, so a script cannot call them like functions. What a script can do is read and replay their parameters, which every generated object carries.

showGearDialog(o) · showScrewDialog(o) · showNutDialog(o) · showPipeDialog(o) · showToreDialog(o) · showCylindDialog(o) · showLatheDialog(o) · showArcSphereDialog(o) · showCubeChanfreinDialog(o) · showCircularTextDialog(o)
Each takes an optional existing object to reopen in edit mode with its genParams restored. The matching hide*Dialog(cancel) closes them.
DOM
editSelectedGen()
Reopens the right dialog for whatever is selected, dispatching on genType. One call instead of ten branches.
selectionDOM
_gearBuild(p) · _pipeBuild(p) → {vPos, tris} · _getGearParams() · _getPipeParams()
The pure builders sitting under the dialogs: give them a parameter object shaped like genParams and they return raw arrays, no DOM involved. Feed the result to _finalizeGenToScene to place it.

genParams by generator

genTypeGeneratorParameters
gearGear.GengearMode, gearType, gearModule, gearTeeth, gearHeight, gearHoleR, gearTwist, gearHand, gearRes, gearBacklash, gearPair, gearTeeth2, pulleyType…
screwScrew.Gensystem, specIdx, thread, pitchCustom, length, head, nRad, chamferAbout
nutNut.Gensystem, specIdx, thread, pitchCustom, style, mCustom, chamfer, nRad
pipePipe.Genre, ep, rb, sc, sr, l1, v1y, v1z, l2, v2y, v2z, l3
toreTore.GenR, r, N, M
cylindCylind.GenH, Rt, Rb, ct, cb, segs, mode, N
cubicCubic.GenW, H, D, c, segs, mode
arcsphereArcSphere.GenR, Wphi, Htheta, arc
latheRevSolid.GenR, H, N, arc
circtextCircularText.Gentext, fontKey, size, depth, curve
sketchSketch.Genentities[], depth
The reliable shortcut

Build the part once by hand, then read objs.at(-1).genParams from a script. You now have the exact, valid parameter object — vary it in a loop and hand each variant to the matching builder, or store it and rebuild the part later without touching the dialog.

12Import

Importers take a File object, so a script can feed them from anywhere — a fetch, generated text, the clipboard.

importSTEP(file) · importSTEP_XCAF(file) · _importSTEPUnified(file)
STEP AP203 / AP214 / AP242. The XCAF path adds assembly structure, per-part and per-face colours read from the file itself, and PMI / GD&T. Chunking, an IndexedDB cache and a worker pool are handled internally.
asyncOCCT
importGLB(file, opts) · import3MF(file, opts)
GLB and .gltf (embedded buffers) with Draco decoding when present — glTF metres are converted to millimetres, one object per node, material colours kept. 3MF with its unit, components, production-extension parts and base/colour-group colours. Both keep the file's layout as an assembly. opts.noUndo skips the undo snapshot (the batch importer already took one).
async
parseSTL(buf, opts) · parseOBJ(txt, opts) · parsePLY(buf, opts)
The raw parsers, separate from the file layer — the way to inject a mesh you already hold in memory. They throw on a file without any face. Extra data rides in geometry.userData: parts (OBJ o/g, multi-solid STL, as triangle ranges), faceRGB (per-triangle colour) and color (STL header colour). parsePLY returns an indexed geometry.
async
importMesh(event) · doImp(accept) · loadFile(event)
The UI path: doImp opens the picker, importMesh processes the batch strictly in sequence with a watchdog sized from the largest file.
DOM
stepSliceBySize(text, maxChunkBytes) · stepSliceByComponentsAndSize(text, maxChunkBytes)
The two chunking strategies for very large STEP files — by raw size, or along component boundaries first.
nasFaceColorReport(n) · nasFaceColorReset()
Diagnostics for per-face colour decoding: what was recognised, what failed and why.
nasStepDeclared(text|buffer) · nasStepDeclaredAlpha(decl, '#rrggbb') · nasStepAudit(decl, result)
What the file declares, read straight from the Part 21 text before any geometry: solids, shells, edge valence — and styleAlpha, the colour → opacity table. No reader (MEDUSA, occt-import-js, the NSPG cache) carries an alpha channel, so that table is what makes a translucent body come in translucent. A hue seen both opaque and translucent is ambiguous and stays out of the table; nasStepDeclaredAlpha tolerates ±1 per channel on lookup, the rounding the sRGB round-trip can cost.
// import a STEP file from a URL without touching the file picker
const r = await fetch('https://example.local/flange.stp');
const f = new File([await r.blob()], 'flange.stp', {type:'application/step'});
await importSTEP_XCAF(f);
return objs.length + ' objects after import';

13Export

Scope

Every mesh exporter — STL, OBJ, 3MF, GLB, PLY — walks objs, meaning the entire scene, never the selection. STEP is the one exception: its internal entry point takes an explicit object list.

expSTL() · expSTLascii() · expOBJ() · expPLY() · exp3MF() · expGLB(useDraco)
Mesh exports at setExpQuality resolution. STL (binary and ASCII) and 3MF run a watertightness gate that can prompt. GLB is written in metres, as glTF requires, with one material per colour; 3MF is deflate-compressed with object and per-face colours; PLY is binary with vertex colours. They open the native save picker where the browser supports it, otherwise fall back to a download.
async
expSTEP() · openStepExportModal() · doStepExport() · closeStepExportModal()
Opens the STEP dialog — protocol, fusion mode, custom tolerance, stats — then runs the export with those settings. The picker is deliberately opened during the click gesture, before the B-Rep computation. The file is written as an assembly: one PRODUCT per body, named after it, tied to the root by NEXT_ASSEMBLY_USAGE_OCCURRENCE — a single body stays a single product. Colours travel as a STYLED_ITEM per body plus an OVER_RIDING_STYLED_ITEM per face that differs from it, and opacity below 1 as SURFACE_STYLE_RENDERING_WITH_PROPERTIES.
asyncDOM
_expSTEPRun(objList, {returnText, silent, fileHandle}) → Promise
The single best scripting hook in the API. Writes STEP for an arbitrary object list and, with returnText: true, resolves with the text instead of downloading; with silent: true, without the spinner. Settings come from globalThis._stepExportConfig. Passing no list defaults to the whole scene.
async
_stepExportConfig · STEPApVersions · STEPFusionModes
Config object read at export time: {fusionMode, apVersion, customTolerance, logStats}. The two tables below list the accepted values.
_doSectionExp('svg'|'dxf') · _sliceMesh(geo, planeY)
Horizontal cross-section of the whole scene at the Y read from the dialog field, seen from above, emitted as chained contours (closed SVG paths, closed DXF R12 polylines). _sliceMesh is the underlying triangle/plane intersector and returns plain segments; _chainSegments joins them.
DOM
saveProject() · saveProjectAs() · loadProject() · newProject()
Native project I/O. newProject() asks for confirmation only when the scene is non-empty, then disposes everything.
async
serializeGeo(g) · deserializeGeo(d) · f32ToB64 · b64ToF32 · u32ToB64 · b64ToU32
Compact geometry serialisation, the same one the project format and the CSG tree use. Everything you need to stash or transport a mesh from a script.
apVersionSchemaColoursUse
AP203Config Controlled Designyes (+ Shape Appearance Layer MIM)widest reader compatibility
AP214Automotive Designyesthe industry default
AP242Managed Model Based 3D Eng.yescurrent standard, MBD / PMI capable
fusionModeToleranceDecimalsEffect
EXACT1e-66strictest coplanar merge
ROBUST1e-55default; survives noisy meshes
FACETEDno merge — faceted B-Rep, for organic shapes
Decimals quantise the vertex and plane keys of the merge; customTolerance overrides both. The tolerance is also written as the file's UNCERTAINTY_MEASURE_WITH_UNIT. Coordinates themselves are always written to six decimals.

14Scene, undo, camera, units

undoPush(label) · undoAction() · redoAction() · idbJournalPurge() · UNDO_JOURNAL_MAX = 200
All three are async. await undoPush('my script') at the top guarantees the restore point is written before you change anything. Snapshots are captured synchronously then persisted to IndexedDB.
async
camA {theta, phi, dist} · camT · updCam() · setCameraView('top'|'front'|'right'|'persp')
Spherical orbit and target. Write into camA, then call updCam(). persp also resets the target to the origin at distance 240.
setDispUnit('mm'|'in'|'ft') · MM_PER_IN · MM_PER_FT
Display unit. The model stays in millimetres — but setTr and setDim interpret their arguments in this unit, and the choice is persisted.
togSnap() · setSnapSize(sz)
Snapping on/off and its step. togSnap() cycles through SNAP_SIZES and only turns snapping off after the last one.
showSpinner(label, sub, progress) · hideSpinner(force)
The progress overlay — the honest way to make a long script visible. Always pair it with hideSpinner() in a finally.
init() · anim() · updatePerf() · updGizmo() · updateCotation() · onRz()
Bootstrap, the render loop, the FPS/perf readout, the rotation gizmo, the dimension overlay, and the resize handler. Setting _camDirty = true is what asks for a redraw.
mDown(e) · mMove(e) · mUp(e) · mWheel(e) · mDblClick(e) · onKey(e)
The raw input handlers. They expect real events; calling them with synthetic ones is possible but there is usually a direct function that does what you actually want.

15Memory & storage

GeometryPool {init, geoStore, geoLoad, snapSave, snapRestore, free, gc, stats, initialized}
A single pre-allocated ArrayBuffer split into a slot header, a geometry zone (80 %) and a snapshot zone, with bump allocation and mark-and-sweep collection. stats() returns {totalMo, geoUsedMo, geoFreeMo, snapUsedMo, snapFreeMo, slots, maxSlots}.
flushPool() · updPoolStats() · setPoolSize(mb) · enableAutoPool()
Force a collection and report, refresh the readout, resize the pool, or hand sizing back to the automatic mode.
idbInit() · idbTx(store, mode) · idbLogSave(entry) · idbUpdateQuotaDisplay() · idbSetStatus(msg, color)
The IndexedDB layer behind the undo journal, the log history and the STEP cache — database NASSCAD_DB, 2 GB quota target, persistent storage requested at startup.
async
idbSaveSettings(key, val) · idbLoadSettings()
Persistent settings. A script can park its own keys here to survive a reload — the one storage in NASSCAD that outlives the session without touching a file.
async

16Logging & diagnostics

nasLog(level, message)
Levels: ERROR WARN OK INFO CSG IDB MEDUSA DBG. All are persisted to IndexedDB except DBG, deliberately, as too noisy — so a script message you want to keep must not use that level. console.log/warn/error, uncaught errors and unhandled rejections are all funnelled here automatically.
toggleLogWin() · setLogFilter(f) · logClear() · logExport() · logCopyClipboard() · logRecallIDB(n)
The log window: open, filter, clear, export to a timestamped file, copy, or recall earlier sessions from IndexedDB. 2 000 entries live in RAM. The MEDUSA filter is the one cross-cutting filter — it also catches engine lines logged at other levels.
async
medusaLogPull(nMax)
Pulls the native engine's log (GET /log) and renders it beside the browser log. MEDUSA keeps 20 000 lines in memory and writes no file unless started with --logfile.
asyncMEDUSA
async await it selection acts on selObjs MEDUSA native engine required OCCT WASM kernel DOM dialog must be open undo pushes a restore point

17Bundled libraries

Loaded as plain globals alongside the app, and therefore available to any script.

THREE
The full three.js namespace. Everything NASSCAD builds is ordinary three.js underneath, so BufferGeometry, Box3, Matrix4, Raycaster and the loaders are all fair game.
DracoDecoderModule(cfg) · DracoEncoderModule(cfg)
Draco codec, used by GLB import and by Draco-compressed GLB export. Both are instantiated lazily from an inlined base64 WASM payload.
occtimportjs(cfg)
The occt-import-js bridge — the lighter STEP/IGES reader, distinct from the full OCCT kernel that _occtLoad() instantiates.

18Recipes

Ten complete scripts. Each was executed against a running NASSCAD 4.7.0 instance while writing this page; the numbers quoted in the comments are the values that came back.

1 — Scene inventory

What the file actually contains: type, origin, triangle count, footprint.

const rows = objs.map(o => {
  const g = o.mesh.geometry;
  const tri = g.index ? g.index.count / 3 : g.attributes.position.count / 3;
  o.mesh.updateMatrixWorld(true);
  const s = new THREE.Box3().setFromObject(o.mesh).getSize(new THREE.Vector3());
  return [
    String(o.id).padStart(4),
    o.name.padEnd(26).slice(0, 26),
    (o.genType || o.type).padEnd(10),
    (Math.round(tri) + ' tri').padStart(12),
    `${s.x.toFixed(1)}×${s.y.toFixed(1)}×${s.z.toFixed(1)} mm`,
    o.isHole ? '  hole' : ''
  ].join('  ');
});
scriptLog(rows.join('\n'));

const byKind = {};
objs.forEach(o => { const k = o.genType || o.type; byKind[k] = (byKind[k] || 0) + 1; });
scriptLog('\n— breakdown —', byKind);
return objs.length + ' objects';

2 — Real volume and mass

Signed mesh volume including the world transform, converted to mass via the applied material. Verified: a default cube returns exactly 8 000 mm³.

// densities in g/cm³, keyed on NASSCAD material ids
const RHO = {alu:2.70, inox:7.90, acier:7.85, laiton:8.50, cuivre:8.96,
             or:19.30, titane:4.51, 'abs-noir':1.04, 'abs-blanc':1.04,
             caoutchouc:1.20, carbone:1.60, verre:2.50};

function volumeMm3(mesh){
  mesh.updateMatrixWorld(true);
  const g = mesh.geometry, p = g.attributes.position.array;
  const ix = g.index ? g.index.array : null;
  const n = ix ? ix.length / 3 : p.length / 9;
  const m = mesh.matrixWorld;
  const a = new THREE.Vector3(), b = new THREE.Vector3(), c = new THREE.Vector3();
  let v = 0;
  for(let i = 0; i < n; i++){
    const i0 = (ix ? ix[i*3] : i*3) * 3;
    const i1 = (ix ? ix[i*3+1] : i*3+1) * 3;
    const i2 = (ix ? ix[i*3+2] : i*3+2) * 3;
    a.set(p[i0], p[i0+1], p[i0+2]).applyMatrix4(m);
    b.set(p[i1], p[i1+1], p[i1+2]).applyMatrix4(m);
    c.set(p[i2], p[i2+1], p[i2+2]).applyMatrix4(m);
    v += a.dot(b.clone().cross(c)) / 6;   // signed tetrahedron (O,a,b,c)
  }
  return Math.abs(v);
}

let total = 0;
(selObjs.length ? selObjs : objs).forEach(o => {
  const mm3 = volumeMm3(o.mesh);
  const rho = RHO[o.matId] ?? 1.0;
  const g = mm3 / 1000 * rho;           // mm³ → cm³ → g
  total += g;
  scriptLog(`${o.name.padEnd(24)} ${(mm3/1000).toFixed(2).padStart(10)} cm³ `
    + `${g.toFixed(1).padStart(9)} g   ${o.matId || '(no material, ρ=1)'}`);
});
return 'total mass: ' + (total/1000).toFixed(3) + ' kg';

3 — Parametric hole grid

Select the plate, run: holes are created, aligned to its bounding box, and subtracted in one operation.

const D = 6, PITCH = 20, MARGIN = 12;        // hole Ø, spacing, edge margin (mm)

const plate = selObjs[selObjs.length - 1];
if(!plate) throw new Error('Select the plate first');
if(!await _medusaProbe(2000)) throw new Error('MEDUSA offline — the subtraction would fail');

await undoPush('hole grid');
plate.mesh.updateMatrixWorld(true);
const bb = new THREE.Box3().setFromObject(plate.mesh);
const thk = bb.max.y - bb.min.y;
const cy  = (bb.max.y + bb.min.y) / 2;

setHoleMode(true);
const holes = [];
for(let x = bb.min.x + MARGIN; x <= bb.max.x - MARGIN + 1e-6; x += PITCH){
  for(let z = bb.min.z + MARGIN; z <= bb.max.z - MARGIN + 1e-6; z += PITCH){
    scriptCheckStop();
    addPrimitive('cylinder');
    const t = objs[objs.length - 1];
    t.mesh.scale.set(D / PS, (thk + 4) / PS, D / PS);   // base is Ø20 × h20 = PS
    t.mesh.position.set(x, cy, z);
    _invalidateBbox(t.mesh);
    holes.push(t);
  }
}
setHoleMode(false);

selObjs = [plate, ...holes];
updProps(); updOList(); updCSG();
await doCSG('subtract');
return holes.length + ' holes Ø' + D + ' mm';

4 — Bolt circle

The base pattern of every flange: N holes on a given circle, left as holes ready to subtract.

const N = 8, PCD = 80, D = 8, H = 30;   // count, pitch circle Ø, hole Ø, height

await undoPush('bolt circle');
setHoleMode(true);
const made = [];
for(let i = 0; i < N; i++){
  scriptCheckStop();
  const a = i * 2 * Math.PI / N;
  addPrimitive('cylinder');
  const t = objs[objs.length - 1];
  t.mesh.scale.set(D / PS, H / PS, D / PS);
  t.mesh.position.set(PCD/2 * Math.cos(a), 0, PCD/2 * Math.sin(a));
  _invalidateBbox(t.mesh);
  made.push(t);
}
setHoleMode(false);
selObjs = made; updProps(); updOList(); updStats();
return `${N} holes Ø${D} on Ø${PCD}`;

5 — Your own geometry into the scene

A hexagonal prism computed by the script and injected as a first-class object. Verified: 30 × 12 × 34.64 mm for 30 mm across flats.

// extrude a convex polygon [[x,z],…] to height h, base at Y=0
function extrude(poly, h){
  const v = [];
  const tri = (...p) => v.push(...p);
  const n = poly.length;
  for(let i = 0; i < n; i++){                // side walls
    const a = poly[i], b = poly[(i+1) % n];
    tri(a[0],0,a[1], b[0],0,b[1], b[0],h,b[1]);
    tri(a[0],0,a[1], b[0],h,b[1], a[0],h,a[1]);
  }
  for(let i = 1; i < n - 1; i++){            // caps (triangle fan)
    tri(poly[0][0],0,poly[0][1], poly[i+1][0],0,poly[i+1][1], poly[i][0],0,poly[i][1]);
    tri(poly[0][0],h,poly[0][1], poly[i][0],h,poly[i][1], poly[i+1][0],h,poly[i+1][1]);
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.Float32BufferAttribute(v, 3));
  g.computeVertexNormals(); g.computeBoundingBox();
  return g;
}

const AF = 30, TH = 12;                       // across flats, thickness
const R = AF / 2 / Math.cos(Math.PI / 6);
const hex = Array.from({length: 6}, (_, i) => {
  const a = i * Math.PI / 3 + Math.PI / 6;
  return [R * Math.cos(a), R * Math.sin(a)];
});

const obj = _finalizeGenToScene({
  label: 'NassScript prism',
  geo: await postProcessCSGGeo(extrude(hex, TH)),   // weld + crease normals
  name: n => `hex${AF}_${n}`,
  genType: 'script',
  genParams: {acrossFlats: AF, thickness: TH},
  hideDialogFn: () => {}
});
return obj.name;

6 — One STEP file per part, no dialog

Verified: a single default cube exports to 7 694 bytes of AP242 with a MANIFOLD_SOLID_BREP body.

globalThis._stepExportConfig = {fusionMode:'ROBUST', apVersion:'AP242', logStats:false};

const targets = selObjs.length ? [...selObjs] : [...objs];
const written = [];
for(const o of targets){
  scriptCheckStop();
  const text = await _expSTEPRun([o], {returnText:true, silent:true});
  written.push({name: o.name, bytes: text.length});

  // comment this block out for a dry-run inventory
  const url = URL.createObjectURL(new Blob([text], {type:'application/step'}));
  const a = Object.assign(document.createElement('a'),
    {href:url, download: o.name.replace(/[^\w.-]/g, '_') + '.stp'});
  a.click(); URL.revokeObjectURL(url);
  await new Promise(r => setTimeout(r, 350));   // let the browser breathe
}
scriptLog(written);
return written.length + ' STEP files';

7 — One STL per part

Mesh exporters only see objs. Swap in a one-object scene, and restore it whatever happens.

const saved = [...objs];
const targets = selObjs.length ? [...selObjs] : [...objs];
try {
  for(const o of targets){
    scriptCheckStop();
    objs = [o];                       // ← objs is a reassignable global
    scriptLog('exporting ' + o.name);
    await expSTL();                   // opens the save picker
  }
} finally {
  objs = saved;                       // ← non-negotiable
  updOList(); updStats(); updProps();
}
return targets.length + ' parts exported';

8 — Pre-flight check

Five seconds that stop you launching a batch of booleans that cannot succeed.

const report = [];

const medusa = await _medusaProbe(2500);
report.push((medusa ? '✓' : '✗') + ' MEDUSA '
  + (medusa ? 'reachable' : 'OFFLINE — no boolean will run'));

const nonManifold = objs.filter(o => o.isManifold === false);
report.push((nonManifold.length ? '⚠' : '✓') + ' non-manifold: '
  + (nonManifold.map(o => o.name).join(', ') || 'none'));

const heavy = objs.map(o => {
  const g = o.mesh.geometry;
  return {n: o.name, t: g.index ? g.index.count/3 : g.attributes.position.count/3};
}).filter(x => x.t > 50000);
report.push((heavy.length ? '⚠' : '✓') + ' over 50k triangles: '
  + (heavy.map(x => `${x.n} (${Math.round(x.t)})`).join(', ') || 'none'));

const s = GeometryPool.initialized ? GeometryPool.stats() : null;
if(s) report.push(`· pool ${Math.round(s.geoUsedMo + s.snapUsedMo)} / ${Math.round(s.totalMo)} MB`
  + `, ${s.slots}/${s.maxSlots} slots`);

report.push(`· ${objs.length} objects, ${objs.filter(o => o.isHole).length} hole(s)`);
scriptLog(report.join('\n'));
return medusa ? 'ready' : 'blocked';

9 — Read and replay generator parameters

Every generated part carries its exact inputs. Dump them, edit them, rebuild from them.

// 1. dump every generated part's parameters as JSON you can keep
const sheet = objs.filter(o => o.genType).map(o => ({
  name: o.name, genType: o.genType, params: o.genParams,
  at: [+o.mesh.position.x.toFixed(3), +o.mesh.position.y.toFixed(3), +o.mesh.position.z.toFixed(3)],
  rot: [+o.mesh.rotation.x.toFixed(4), +o.mesh.rotation.y.toFixed(4), +o.mesh.rotation.z.toFixed(4)]
}));
scriptLog(JSON.stringify(sheet, null, 1));

// 2. rebuild a family of gears from one existing gear's parameters
const ref = objs.find(o => o.genType === 'gear');
if(ref){
  await undoPush('gear family');
  for(const z of [12, 18, 24, 36]){
    scriptCheckStop();
    const p = {...ref.genParams, gearTeeth: z, gearPair: false};
    const {vPos, tris} = _gearBuild(p);
    const g = new THREE.BufferGeometry();
    g.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(vPos), 3));
    if(tris) g.setIndex(Array.from(tris));
    g.computeVertexNormals(); g.computeBoundingBox();
    _finalizeGenToScene({label:'Gear.Gen (script)', geo:g,
      name: n => `gear_z${z}_${n}`, genType:'gear', genParams:p,
      hideDialogFn: () => {}, shininess: 12, specular: 0x2a2a2a});
  }
}
return sheet.length + ' generated parts catalogued';

10 — Drive the 2D sketcher

Read the contour, transform it numerically, extrude — the only fully scriptable modelling loop in the app.

openSketcher();                       // draw a closed contour first, then run this

const ents = SK.getEntities();
if(!ents.length) throw new Error('Empty sketch — draw a closed contour first');

scriptLog(`${ents.length} entities`);
const kinds = {};
ents.forEach(e => kinds[e.type] = (kinds[e.type] || 0) + 1);
scriptLog(kinds);

// bounding box of the sketch, in sketch units
const xs = [], ys = [];
ents.forEach(e => {
  [[e.x1, e.y1], [e.x2, e.y2], [e.cx, e.cy]].forEach(([x, y]) => {
    if(typeof x === 'number') xs.push(x);
    if(typeof y === 'number') ys.push(y);
  });
});
scriptLog(`extent ${(Math.max(...xs)-Math.min(...xs)).toFixed(2)}`
        + ` × ${(Math.max(...ys)-Math.min(...ys)).toFixed(2)}`);

// nudge one entity numerically, then extrude
// SK.updateProp(ents[0].id, 'x2', 45);
SK.extrude();
return objs.at(-1).name;

19Pitfalls

SymptomCauseFix
Scene is right, object list is wrongobjs / selObjs mutated without a refreshupdOList(); updStats(); updProps();
Clicking selects the wrong thingMesh moved by hand, bounding-box cache stale_invalidateBbox(o.mesh)
typeof window.objs is undefinedTop-level const/let never reach the global objectUse the bare name objs
A variable vanishes between runsYour const/let are local to the runwindow.myVar = …
setTr moves 25.4 mm instead of 1Display unit is inchessetDispUnit('mm') · or mesh.position
setDim only affects one objectIt sees selObjs.at(-1) onlyLoop and reassign the selection
Asked for union, got a subtractionA hole object was in the selectionsetHoleMode(false) on the sources
Stop button does nothingNo scriptCheckStop() in the loopOne per iteration
Ctrl+Z undoes the previous actionundoPush not awaitedawait undoPush('…')
The 200-step undo journal fills instantlyaddPrimitive pushes one undo per callBuild geometry and use _finalizeGenToScene
Export contains the whole sceneMesh exporters walk objsRecipe 7, or _expSTEPRun
After N fillets nothing worksWASM heap full of embind instances.delete() every instance
The browser freezes mid-scriptLong synchronous block on the main threadawait new Promise(r=>setTimeout(r,0))
SK is undefinedSketcher module not reached yetIt exists from load; check for a script error at startup
Custom geometry renders inside-outTriangle winding reversedSwap two vertices per triangle, recompute normals

20How this was verified

Static reading finds what is declared. It does not tell you what is actually reachable, which is the only thing a script cares about.

Step 1

Source pass

  • 10 shipped files parsed
  • 678 top-level function declarations
  • 243 without a leading underscore
Step 2

Live enumeration

  • App served over HTTP, headless browser
  • Global scope diffed against a blank page
  • 102 sketcher functions found absent
Step 3

Execution

  • All 11 primitives created
  • Recipes 1, 2, 5, 6, 8 run for real
  • Async flags read off the functions

That second step is what corrected the picture: the sketcher's internals never reach the global scope, top-level const bindings never land on window, and toggleXRay turned out to be async while _expSTEPRun and _occtLoad return promises without being declared async. The figure of 576 reachable global functions, 189 of them public, is the source count minus the isolated module.

Numbers quoted elsewhere on this page — a 12-triangle cube, a 2 520-triangle sphere at 32 segments, 8 000 mm³ for a default cube, 7 694 bytes of AP242 for one cube, a 30 × 12 × 34.64 mm hexagonal prism — came back from that running instance, not from reading the code.