Skip to content

The language

Every size in a recipe is either a number or a formula. A formula is one line, it reads names, and it has no variables, no loops and no side effects — which is why the same cabinet comes out of a browser and out of a server byte for byte.

Name What it is
width, height, t, … your parameters, by key
inner_w, gap, … your derived values, by key
p.width, d.inner_w the same two, said explicitly
i, n inside a repeat: which instance, and how many
part.side.w a part already computed — its w, h, d, x, y, z
mat.body.t the thickness of the material in a slot
a require target a bare name a rule switched on

Order matters and it is the order on the page: parameters, then derived, then parts in the order you wrote them. part.side.w works in the bottom because the side is written above it; the other way round it reads nothing.

{ "key": "width", "type": "integer", "default": 600,
"range": { "from": 300, "to": 1200, "step": 1 },
"presets": [400, 500, 600, 800, 900, 1200],
"unit": "mm", "role": "size.w", "affectsSku": true }
  • range for a continuum, options for a list — never both. A list gets a row of buttons; a range gets a slider and a field to type 637 into.
  • editable: "technologist" keeps a working number out of the customer’s panel. Panel thickness usually is one.
  • role tells the engine which parameter is the cabinet’s width, height and depth. Without it the module has no nominal size and nothing snaps to it.
  • affectsSku decides whether two cabinets that differ only in this become two lines of the bill or one.

A number the customer chooses is a parameter, not a derived value. It is tempting to write doors: "width > 600 ? 2 : 1" — and then somebody who wants two narrow doors on a 600 has nowhere to say so. Make it a parameter with options [1, 2] and let a rule correct the impossible combinations.

"derived": { "inner_w": "width - 2*t", "gap": 3, "back_t": 4 }

Computed once, in the order written, and they may read the ones above them. A cycle is refused when you publish rather than discovered as a cabinet full of zeroes.

A name may not be both a parameter and a derived value. The evaluator would silently prefer one, and your recipe would work while computing the other number.

Four actions, and choosing between them is the whole craft:

{ "id": "drawers_need_height", "when": "front == 'drawers3' && height < 660",
"action": "refuse",
"message": { "en": "Three drawers do not fit below 660 mm",
"ru": "Три ящика не помещаются ниже 660 мм" } }
Action Use it when What the customer sees
refuse there is no sensible answer the change does not take, and your message says why
correct there is exactly one sensible answer the value changes to it, with your message
require a part appears only sometimes nothing; the part is simply there
warn it is buildable and worth saying your message, beside the piece

A refusal must be a sentence a customer can act on. “Three drawers do not fit below 660 mm” tells them what to change. “Invalid configuration” does not. Your message is shown in their language; write both.

correct runs up to three times — a correction changes what the next rule sees. Two rules that undo each other are caught and reported rather than looping.

clamp(v, lo, hi) snap(v, step) even(v) odd(v) if(cond, a, b)
spread(i, n, from, to)
hingePositions(i, h, qty)
drawerHeights(i, h, n, gap)
mm(v)

Each returns the i-th of n rather than a list — the language has no lists — so they are written to be used inside a repeat:

{ "id": "shelf", "repeat": "shelves",
"at": ["t", "spread(i, n, t, height - t)", "0"] }

spread reaches both bounds. spread(0, 3, 0, 900) is 0 and spread(2, 3, 0, 900) is 900. That is right for hinges on a door edge and wrong for shelves: written between the bottom and the top panels, the first shelf lies ON the bottom and the last presses against the top. Two shelves that are not shelves, in the cut list, in the price, and overlapping nothing.

Count the gaps instead:

"derived": { "shelf_gap": "(inner_h - shelves*t) / (shelves + 1)" },
"at": ["t", "t + shelf_gap + i*(shelf_gap + t)", "0"]

The same arithmetic places dividers along the width. A divider sits on the boundary BETWEEN two bays, which is not a point spread produces once the divider has a thickness.

A recipe may not exceed 400 parts after repeat is expanded, and one evaluation has a budget of 200 000 steps. Both are refusals at publish time, not surprises at cut time.