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.
Names a formula can read
Section titled “Names a formula can read”| 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.
Parameters
Section titled “Parameters”{ "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 }rangefor a continuum,optionsfor 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.roletells 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.affectsSkudecides 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 values
Section titled “Derived values”"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.
The nine functions
Section titled “The nine functions”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.
Limits
Section titled “Limits”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.