Skip to content

Design of formulas

  • simplest mental model
  • read [[algebra]]
  • all plain attributes (eg, "x") refer to self SO
  • dot-prefix (eg, ".x") refers to parent SO
  • SO name.attribute name -> reference a non-parent SO (eg, "A.x")
  • for attributes with invariant = true -> use default derived formula
    • derived formulas use bare self-refs (eg "X - w")
  • empty formula for position attributes -> silently use parent.attribute + value
    • empty formula for length attributes -> silently use value

Bugs

  • 2' resolves to 1' 12"
  • value column too narrow, increase by 20px
  • values for start in all axes for root MUST ALWAYS be ZERO
  • for attributes with formulas, disable the value cell
  • stretch a parent -> rotates children (ack!)
  • restore on launch also rotates children
  • invariant flag ignored during resolve → added enforce_invariants()
  • w value shows geometry instead of attribute value → resolve rule: always read .value
  • X row shows old user formula after invariant change → set_invariant must clear formula first
  • persisted formula on invariant attribute survives reload → rebind_formulas clears invariant formulas at top
  • compound imperial .w - 1 1/2" didn't parse → added try_compound_feet/inches to Tokenizer
  • algebra fails 1" + 2" -> 1'
  • 3' - 1/4" -> does not work in value cells
  • display formulas with spaces between tokens (eg. ".l - front_th + dado")

Examples

For the "x" row:

FormulaResolves to
(empty)parent.x_min + x_min
x * 2self.x_min * 2
.x * 2parent.x_min * 2
A.x * 2A.x_min * 2
.x + A.Xparent.x_min + A.x_max

Proposal

Mental model: x means "my own x." .x means "parent's x" (dot-prefix, like A.x with name omitted). Only name another SO when you mean something other than your parent. Empty formula = parent.attribute + value — the default relationship every child already has.

Per architecture, aliases go in Constraints.resolve and .write. Tokenizer, compiler, evaluator stay untouched.

Four changes, in order

  1. Alias map in Constraintsresolve_alias(attribute) maps xx_min, yy_min, w → computed width, etc. Called inside resolve() before get_bound(). Derived aliases (w, h, d) compute from two bounds (x_max - x_min). Reverse propagation: writing w = 300 sets x_max = x_min + 300.

  2. Bare attribute → parent reference — bare identifier like x (no dot) currently throws. Change: bare identifier → reference to parent SO with that attribute. x * 2 tokenizes as { type: 'reference', object: <parent_id>, attribute: 'x' }. Needs parent id passed into tokenizer context or resolved later in Constraints.

  3. Empty formula default — when formula is empty/null, Constraints silently treats it as parent.<attribute> + <current_value>. The identity relationship — child at offset from parent.

  4. Invariant-derived formulas — when an attribute has invariant = true, it's not a source of truth — it's derived from the other two in its axis. Constraints generates its formula from the alias map's "For Invariant" column: if x is invariant, xX - w (position derived from far edge minus width). Only one attribute per axis can be invariant. The other two are edited directly; the invariant one recomputes.

Tests

  • Alias resolution: xx_min, Xx_max, w → width
  • Bare attribute: x * 2 with parent context → parent.x_min * 2
  • Cross-SO reference: A.x * 2A.x_min * 2
  • WRONG: Empty formula: evaluates to parent.x_min + value
  • Reverse propagation through aliases: change result, w updates x_max
  • Invariant default: marking x as invariant gives it formula X - w; changing X or w recomputes x
  • Only one invariant per axis: marking x invariant means w and X stay editable