The .openchip file format
A .openchip file is plain JSON, pretty-printed with two-space indentation
and a trailing newline. There is no compression, no binary blob, and no wrapper:
the file is the project object. You can read it, diff it, and put it in a
repository next to the paper.
{ "formatVersion": 1, "meta": { "name": "Counter-flow exchanger", "units": "mm", "...": "..." }, "printerProfile": { "...": "..." }, "chipParams": { "...": "..." }, "channelGraph": { "nodes": [], "edges": [] }, "fluidProfiles": [], "fluidSetup": { "...": "..." }, "savedImages": [], "drainageVents": [], "presetLibraryRef": null}Two rules the format is built on
Section titled “Two rules the format is built on”Unknown fields survive. Every container is validated loosely, so a file written by a newer build opens in an older one and keeps its unrecognised keys through a save round-trip. Silently deleting data because you opened a file in last month’s build would be data loss, and this project is not going to do that to anyone’s design.
Missing fields get defaults, not errors. Anything added after the first release is optional on read and filled in with a documented default. A file from before drainage vents existed still opens.
The exception is the block below: these are required, and a file without them is rejected with a message naming the field.
Top level
Section titled “Top level”OpenChipProject
| Field | Type | Required | Notes |
|---|---|---|---|
formatVersion |
positive integer | yes | Currently 1. |
meta |
object | yes | Below. |
printerProfile |
object | yes | Below. |
chipParams |
object | yes | Below. |
channelGraph |
object | yes | Below. |
fluidProfiles |
array | no — defaults [] |
Below. |
fluidSetup |
object | no — defaults to no assignments | Below. |
savedImages |
array | no — defaults [] |
Below. |
drainageVents |
array | no — defaults [] |
Below. |
presetLibraryRef |
string or null | no — defaults null |
The template id this project started from. |
ProjectMeta
| Field | Type | Constraint |
|---|---|---|
name |
string | Not empty. |
createdAt |
string | ISO-8601. |
modifiedAt |
string | ISO-8601. Stamped on save. |
units |
"mm" |
The only accepted value. |
units is a single-member union on purpose. OpenChip works in millimetres
throughout — the canvas, the solver, the exporters — and a file claiming inches
would be a file whose numbers mean something else. If you need imperial, convert
on the way in.
printerProfile
Section titled “printerProfile”PrinterProfile — the machine this design is being checked against. It is stored
in the project, not just in your settings, so a file you send someone else
carries the assumptions it was designed under.
| Field | Type | Constraint |
|---|---|---|
name |
string | Not empty. |
xyResolutionMicrons |
number | > 0. Pixel pitch in the XY plane. |
layerHeightMicrons |
number | > 0. |
minFeatureSizeMm |
number | > 0. Drives channel-below-min-feature. |
buildVolumeMm |
Vec3Mm |
x, y, z, each > 0. Drives footprint-exceeds-build-volume. |
See Printer profile fields for what each one means in practice and how to measure your own.
chipParams
Section titled “chipParams”ChipParams — the block the channels are cut into.
| Field | Type | Default | Constraint |
|---|---|---|---|
width |
number | 40 |
> 0. Chip X extent, mm. |
length |
number | 60 |
> 0. Chip Y extent, mm. |
baseThickness |
number | 4 |
> 0. Total block height, mm. |
wallThickness |
number | 2 |
> 0. Minimum material between a channel and anything else. |
minEdgeSpacing |
number | 3 |
≥ 0. Your own rule for how close anything may sit to the chip edge. |
splitType |
"two-piece" | "monolithic" |
"two-piece" |
See Monolithic vs. two-piece. |
splitHeight |
number | 2 |
≥ 0. Height of the cut plane, mm. |
defaultChannelWidthMm |
number | 0.8 |
> 0. Used where a segment has no dimension marker. |
defaultChannelHeightMm |
number | 0.5 |
> 0. |
defaultCrossSection |
"round" | "rect" | "trapezoid" |
"rect" |
The last three are defaulted rather than required: files written before they existed still open.
wallThickness and minEdgeSpacing are different rules and are often confused.
wallThickness is material between a channel and anything, including another
channel. minEdgeSpacing is specifically about the chip boundary.
channelGraph
Section titled “channelGraph”ChannelGraph — the design itself.
| Field | Type |
|---|---|
nodes |
array of ChannelNode |
edges |
array of ChannelEdge |
Beyond the field types, the loader enforces referential integrity: node ids
must be unique, edge ids must be unique, and every edge’s fromNodeId and
toNodeId must resolve to a node that exists. A dangling reference is rejected
at load rather than crashing the canvas later.
Every node shares three fields (NodeBase) and then discriminates on type.
| Field | Type | Notes |
|---|---|---|
id |
string | Not empty. Unique across the graph. |
x |
number | Finite. Chip millimetres, origin at the bottom-left. |
y |
number | Finite. |
type |
"waypoint" | "port" | "chamber" |
WaypointNode adds nothing. It is a bend or a junction in the path.
PortNode — an opening to the outside.
| Field | Type | Notes |
|---|---|---|
portType |
"syringe" | "luer" | "luer-lock" | "custom" |
|
diameterMm |
number | > 0. The bore — the through-hole into the channel. |
customSpec |
record of string → number | string | Optional. Per-type geometry: taper diameters, socket depth, thread pitch. |
ChamberNode — a widened volume at a node.
| Field | Type | Notes |
|---|---|---|
shape |
"sphere" | "cylinder" |
|
sizeMm |
number | > 0. Diameter. |
autoPlaced |
boolean | True when the app placed it at a corner rather than you. |
ChannelEdge — one channel segment between two nodes.
| Field | Type | Notes |
|---|---|---|
id |
string | Not empty, unique. |
fromNodeId |
string | Must resolve. |
toNodeId |
string | Must resolve. |
curveType |
"straight" | "bezier" |
|
controlPoints |
array of Point2 |
Optional. Kept when you toggle a bezier back to straight, so toggling again restores the curve. |
crossSection |
"round" | "rect" | "trapezoid" |
Per segment, not per chip. |
dimensionPoints |
array of DimensionPoint |
May be empty — the chip defaults then apply. |
Point2 is { x, y }, both finite.
Dimension points
Section titled “Dimension points”DimensionPoint — a width pinned at a position along a segment.
| Field | Type | Constraint |
|---|---|---|
t |
number | 0–1. Position along the segment. |
widthMm |
number | > 0. |
heightMm |
number | > 0. Optional — falls back to the chip default. |
The solid generator interpolates between consecutive dimension points, and across a connected chain of edges when you dimension a whole path. See Dimension markers and lofting.
fluidProfile
Section titled “fluidProfile”FluidProfile — a fluid’s physical properties. Every numeric field is defaulted
from water, so a file with a bare fluid entry still opens.
| Field | Type | Default | Notes |
|---|---|---|---|
id |
string | — | Not empty. |
name |
string | — | Not empty. |
builtIn |
boolean | — | False for fluids you created. |
densityKgM3 |
number | 998.2 |
> 0. |
dynamicViscosityPaS |
number | 0.001002 |
> 0. |
thermalConductivityWmK |
number | 0.598 |
> 0. |
specificHeatCapacityJkgK |
number | 4182 |
> 0. |
inletTemperatureC |
number | 20 |
Finite. Default inlet temperature. |
glycerolVolumePercent |
number | — | Optional. Set on glycerol/water blends so the UI offers the percentage control. |
note |
string | — | Optional. Free-text caveat shown under the fields. |
nonNewtonianNote |
string | — | Optional. Shown prominently when the real fluid is meaningfully non-Newtonian. |
See Fluid property tables for the shipped values and where they come from.
fluidSetup
Section titled “fluidSetup”FluidSetup — how a run is configured, as opposed to what the chip is.
| Field | Type | Default |
|---|---|---|
assignments |
array of FluidAssignment |
[] |
chipMaterialThermalConductivityWmK |
number | 0.2 — typical photopolymer resin |
FluidAssignment — one port’s role in the run.
| Field | Type | Default | Notes |
|---|---|---|---|
portId |
string | — | Must name a port node. |
role |
"inlet" | "outlet" |
— | |
fluidProfileId |
string or null | null |
|
flowRateUlPerMin |
number | 10 |
≥ 0. Ignored for an outlet. |
inletTemperatureC |
number or null | null |
Overrides the fluid’s own default for this run. |
drainageVent
Section titled “drainageVent”DrainageVent — a hole added so uncured resin can escape.
| Field | Type | Notes |
|---|---|---|
id |
string | Not empty. |
nodeId |
string | The node the vent is placed at. |
diameterMm |
number | > 0. Default bore is 0.8 mm. |
autoPlaced |
boolean | True when the drainage planner placed it. |
savedImage
Section titled “savedImage”SavedImage — an export kept with the project.
| Field | Type | Notes |
|---|---|---|
id |
string | Not empty. |
kind |
"schematic-svg" | "cad-snapshot" | "heatmap" |
|
label |
string | |
createdAt |
string | ISO-8601. |
mimeType |
string | Not empty. |
data |
string | The payload — base64 for raster, source text for SVG. |
These are stored inline, so a project with several high-resolution snapshots gets large. They are yours to delete from the Project menu.
Hand-editing a file
Section titled “Hand-editing a file”It is plain JSON and nothing stops you. Two things to know:
- Referential integrity is checked on load, so renaming a node id means renaming it in every edge that references it.
modifiedAtis overwritten on the next save. Don’t rely on it as a record of your edit.
If a file will not open, the error names the field and what it expected — the loader rewrites raw validation paths into sentences for exactly this case.