Project configuration
A FluidCAD project is an ordinary folder: a descriptor, an entry point, and one file per model. Nothing is hidden in a database — the server reads the folder, and everything in it is plain text you can diff and commit.
my-hinge/
├── package.json ← npm dependencies (the fluidcad package)
├── jsconfig.json ← editor type hints
├── fluidcad.json ← project descriptor: engine pin, unit, materials
├── init.js ← required entry point
├── leaf.part.js ← a part
├── pin.part.js ← another part
└── hinge.assembly.js ← an assembly of the two
npx fluidcad init writes the first four; you add the model files as you work. Installation covers creating the project in the first place.
package.json holds the fluidcad dependency — the engine resolves from the project's node_modules, which is why FluidCAD is installed per project rather than globally. jsconfig.json points your editor at that package's type declarations, so extrude( shows its signature outside the FluidCAD editor too.
init.js
The project's entry point. It loads the geometry kernel and creates the scene that every model file writes into, so it must exist at the project root before any .part.js can run:
import { init } from 'fluidcad';
export default await init();
npx fluidcad init generates it, and it is not meant to be edited: the top-level await is what makes the kernel ready before the first sketch statement executes.
For completeness, init() accepts an options object — init({ unit: 'in' }) overrides the project's length unit, and a mesh option sets display density. Those are hooks for embedding hosts and test setups; a normal project sets unit in fluidcad.json and leaves init.js untouched.
fluidcad.json
The project descriptor, written next to init.js: plain JSON that the server, the CLI and the desktop app read without executing any of your code.
{
"engine": "0.0.42",
"unit": "mm",
"materials": {
"alloy-steel": { "name": "Alloy Steel", "density": 7.7, "densityUnit": "g/cm³" }
}
}
Every key is optional. A project with no fluidcad.json behaves as if the file held none of them: no engine pin, millimetres, only the built-in materials.
engine
The FluidCAD version the project's geometry was authored against. npx fluidcad init pins the version it was run with and leaves an existing pin alone.
The pin records intent; it does not switch versions by itself. npx fluidcad serve runs whatever fluidcad is installed in the project, and when that differs from the pin the startup banner says so before anything is rebuilt:
FluidCAD: this project pins engine 0.0.42 (fluidcad.json), but 0.0.45 is running.
Geometry may differ from what the project was authored against.
The desktop app and the start screen of npx fluidcad go one step further and use the pin to choose which engine to load for the project — every project opens on the engine it was authored against, however often FluidCAD itself updates. A new project is pinned to FluidCAD's own engine (the one that ships with the app, or the fluidcad you ran), and the start screen shows it as latest until a newer one arrives. From then on the card reads update. The card's ⋯ → Change engine version… moves the pin in either direction, including to an older engine that is downloaded on demand, and Compare first rebuilds every model on both engines and lists the differences — features that stop or start building, solid counts, volumes — before anything is written. In the desktop app the project also offers the move when it opens: Upgrade, Preview changes, Keep the current engine until the next release, or Don't ask again for that project.
With npx fluidcad serve, bump the pin by hand once you have upgraded fluidcad and checked the model still builds the way you expect.
package.json is accepted as an alternative home for the same pin — { "fluidcad": { "engine": "0.0.42" } } — and fluidcad.json wins when both exist. An engine value that is not a version string is reported and ignored.
unit
The project's length unit. It is the unit of every part file that does not declare its own with unit(), and the unit of assembly space. One of mm, cm, m, in, ft; the default is mm.
npx fluidcad init --unit in
writes "unit": "in". Without the flag, init uses the unit for new projects chosen in the Settings dialog (the gear in the top bar, under Units); that is mm unless you change it. Editing the file by hand is fine too — the change is picked up on the next render, no restart needed.
Numbers in a file are in that file's unit. A part file can opt out of the project unit with a single statement at the top:
import { unit, extrude, sketch, circle } from 'fluidcad/core';
unit('in');
sketch("xy", () => {
circle([0, 0], 1); // 1 in
});
extrude(0.5); // 0.5 in
Assemblies never declare a unit: their offsets and translations are in the project unit, and inserted parts of any unit are scaled into it automatically. The Units guide covers the rules, the fluidcad/units conversion helpers, import/export, and which unit to pick for which feature scale.
materials
The project's own materials, keyed by id. A part picks one with .material('<id>') on its part() statement, and Shape Properties turns the part's volume into a mass with that density.
{
"unit": "mm",
"materials": {
"alloy-steel": { "name": "Alloy Steel", "density": 7.7, "densityUnit": "g/cm³" },
"acme-pla": { "name": "ACME PLA+", "density": 1.27 }
}
}
| Field | Meaning |
|---|---|
| key | The id .material() refers to. Any non-empty string; Settings → Materials slugs one from the name (alloy-steel). |
name | The display name in Shape Properties and the Set material dialog. Required. |
density | A positive number. Required. |
densityUnit | One of g/cm³, kg/m³, g/mm³, lbs/in³. Optional; g/cm³ when omitted. |
The built-in materials (fluidcad-steel-1020, fluidcad-aluminum-6061, fluidcad-pla, … — every id starts with fluidcad-) are always available and are listed under Built-in in every material list; the entries of this map follow under Custom, tagged in project, together with your own materials from Settings → Materials. An entry that reuses a built-in id overrides that built-in for this project.
The map travels with the project: a .material('alloy-steel') in a part resolves the same way on a teammate's machine, in a share link and in the viewer. A part whose id is in neither list still builds; its timeline row shows a warning triangle and Shape Properties reads Unknown material: <id> with no mass.
The map is filled for you: your own materials live in Settings → Materials, and Set material… on a part row copies the picked one into this key when the project does not have it yet, keeping every other key of the file — so a project holds exactly the custom materials it uses. Nothing removes an entry automatically. Editing the JSON by hand works the same; the file is re-read before every render, so no restart is needed either way. See Shape properties.
An invalid map is reported like a bad unit — an entry without a name string, with a density that is not a positive number, with an unknown densityUnit, or with an empty id — and the whole map is ignored until it is fixed. package.json accepts the same map under { "fluidcad": { "materials": { … } } }, and fluidcad.json wins when both exist.
.part.js — parts
A part file is a normal ES module: it imports the statements it uses from fluidcad/core and runs them top to bottom, and the shapes it builds are the model the viewport shows. Bare statements render as a standalone model; wrapping them in an exported part() is what makes the file insertable into an assembly. The convention is one part per file, named after the thing it is — bracket.part.js exports bracket.
// leaf.part.js
import { part, sketch, rect, extrude } from 'fluidcad/core';
export const leaf = part('Leaf', () => {
sketch('xy', () => {
rect([0, 0], 60, 40);
});
extrude(4);
});
The file is live in both directions: modelling in the viewport writes the statements into it, and editing it by hand and saving rebuilds the model.
.assembly.js — assemblies
An assembly file imports part files and places instances of them: insert() to add a part, .grounded() to anchor one, mate() to join two connectors. Offsets and translations here are in the project unit, and parts authored in another unit are scaled in automatically.
// hinge.assembly.js
import { assembly, insert, mate } from 'fluidcad/core';
import { leaf } from './leaf.part.js';
import { pin } from './pin.part.js';
export const hinge = assembly('hinge', () => {
const base = insert(leaf).grounded();
const rod = insert(pin);
mate('revolute', base.connectors.bore, rod.connectors.shaft);
});
An exported assembly() definition can itself be inserted into another assembly as a sub-assembly.
File naming
The extension is what FluidCAD keys on: .part.js is a part file and .assembly.js an assembly, and both appear in the workspace's file tabs, the Insert dialog and the CLI's export targets. A .js file that follows neither convention is ignored by the viewer — it is just a module your model files can import from, which is how reusable objects are shared between parts.