Skip to content

Algebra

Started: 2026-02-07 Status: phases 1–4 done — 377 tests passing

Remaining Work

  • better names for attributes (x,y,z,X,Y,Z,w,h,d)
  • expand algebra with {s, e, l} contextual aliases
    • s/e/l expand to concrete axis aliases based on owning attribute's axis
    • translate button bottom of attributes
      • between explicit and agnostic

Problem

Bounds are independent — change width, nothing else moves. That's fine for direct editing, but useless for constraints like door.height = wall.height - 6". Need an expression engine that compiles formulas into trees, evaluates forward, and propagates changes back.

Goal

Recursive descent compiler. Parse algebraic expressions into a tree. Traverse forward (evaluate) and reverse (propagate). Wire into Smart Objects so attribute formulas actually do something.

Compile Tree

The tree is an AST. Each node has a type and knows how to evaluate itself.

Node types

Node
├── Literal      → constant number (in mm)
├── Reference    → pointer to an SO attribute (e.g., wall.height)
├── BinaryOp     → left op right  (+, -, *, /)
├── UnaryOp      → -expr (negation)
└── Group        → (expr) — parenthesized subexpression

Grammar

expression  →  term (('+' | '-') term)*
term        →  factor (('*' | '/') factor)*
factor      →  '-' factor | atom
atom        →  NUMBER | UNIT_LITERAL | REFERENCE | '(' expression ')'
  • NUMBER = 3.5, 42
  • UNIT_LITERAL = 6", 5', 2.5 mm — parsed via Units.ts into mm
  • REFERENCE = wall.height, door.x_min — dot-path to SO attribute

Example

wall.height - 6" compiles to:

BinaryOp(-)
├── Reference("wall", "height")
└── Literal(152.4)      // 6" = 152.4mm

Files

FileLines
algebra/Node.tsNode types — literal, reference, binary, unary
algebra/Tokenizer.tsString → token stream, handles unit suffixes via Units.ts
algebra/Compiler.tsRecursive descent parser — expression/term/factor/atom
algebra/Evaluate.tsForward eval, reverse propagation, cycle detection
tests/Compiler.test.ts35 tests — tokenizer + parser
algebra/Constraints.tsGlue — formula management, resolve/write, propagation
tests/Evaluate.test.ts26 tests — eval, propagate, cycles
tests/Constraints.test.ts13 tests — formula eval, propagation, serialization
algebra/Orientation.tsCompute orientation from bounds, recompute max bounds from rotation
tests/Orientation.test.ts18 tests — orientation from bounds, use cases S and W, fixed flag

Phase 3 — What Changes

Attribute

  • Add optional formula: string — the raw expression text
  • Add optional compiled: Node — the parsed tree (cached)
  • When formula is set, value becomes computed (read via evaluate)
  • When formula is null, value stays direct (no change from today)

Smart_Object

  • set_bound gains a propagation hook — after setting a bound, walk all SOs looking for formulas that reference this one, re-evaluate them
  • serialize includes formula strings for any attribute that has one
  • deserialize restores formulas and recompiles them

Editor

  • After commit() calls set_bound, trigger propagation across the scene
  • M11 already identified this hook: "after updating a bound, call constraints.propagate(so)"

New: Constraints module

  • Holds the FormulaMap for the scene
  • Provides propagate(so) — the function Editor calls
  • Runs cycle detection before accepting new formulas

Untouched

Geometry, topology, drag logic — all unchanged.

Phase 4 — What Changes

Smart_Object

  • Add fixed: boolean, default true
  • Fixed: rotating the child sets its quaternion, origin stays put. Parent stretches → child moves with its pinned corner, keeps its angle and length.
  • Variable: both endpoints track parent bounds. Parent stretches → angle changes, length changes. Orientation is recomputed from resulting geometry after propagation.

Constraints

  • After propagate() re-evaluates formulas, recompute orientation for variable children. The quaternion comes from the diagonal of the new bounds: atan2(dy, dx) in the plane of the two varying axes.

Events_3D

  • rotate_object() already updates the quaternion. Now it also preserves the origin — min bounds stay fixed, max bounds recompute from the new angle and current length.

Formulas (unchanged)

  • Users write the same expressions as before — no trig, no angles.
  • S: S.x_min = R.x_min, S.x_max = R.x_max, etc. — endpoints pinned, angle is a consequence.
  • W: W.x_min = R.x_min, W.x_max = R.x_min + 200, etc. — one corner pinned, offsets are literals.
  • The algebra engine doesn't know about orientation. It evaluates scalars. The orientation layer sits on top.

Untouched (from phase 3)

Compiler, Tokenizer, Evaluate, Node — no changes.

Phases

  • 1. compile tree — node types, tokenizer, recursive descent parser
    • tokenize literals, references, operators
    • parse precedence (1 + 2 * 3 → mul binds tighter)
    • parse parentheses
    • parse unit literals (6", 5', 2.5 mm)
    • parse references (wall.height)
    • error on malformed input
    • incorporate orientation
    • each unit system must have a default unit — already in Units.ts default_unit_for_system()
    • add a child creates formulas in which the child shares the same origin as parent, and has unit lengths in the selected unit system's default unit
  • 2. traverse and reverse traverse — evaluate forward, propagate backward
    • forward eval with mock SO values
    • reverse propagate simple cases (a = b - 6 → change a, update b)
    • cycle detection
  • 3. incorporate into SO — formula field on Attribute, propagation hook in Editor
    • formula on Attribute triggers eval
    • Editor commit triggers propagation
    • serialize/deserialize formulas
  • 4. incorporate orientation into propagation
    • angle in any given dimension are between 0 and 90 exclusive, most often 45° +/- 15°
    • add a boolean to SO called variable, default is false, meaning fixed
    • rotating a child changes its quaternion, and preserves its origin
    • use case S (variable): a staircase where the bottom and top are fixed relative to the parent. stretch the parent, the angle of the staircase changes according to those fixed relative locations
    • use case W (fixed): set an angle (w°) for a wall W in parent room R. stretch R -> W moves, maintaining its length
  • 5. names of attributes in formulas
    • x, y, z, w, h, d — nine customer aliases (see [[algebra]])
    • X, Y, Z for far edges — no alternatives needed, uppercase is intuitive
    • compound imperial: 1 1/2", 5' 3", 5' 3 1/2"
    • offset model: empty formula = parent.attr + offset
    • invariant clearing: set_invariant and rebind_formulas both clear stale formulas