Appearance
Changelog
0.3.71 — current
=binds loosest and chains. Whatever comes after=is worked out first, as before, and nowa = b = cmeans a = b and b = c, for numbers and for points.=gives back its right side, which is what makes the chain work.
0.3.70
- Arithmetic has precedence.
*and/before+and-, as in any book:2 + 3 * 4is 14,1 - 2 * 3 - 4is −9. Equal ones still go left to right (10 - 2 - 3is 5), and brackets still say otherwise. It comes from the prelude's own definitions —=> Scalar at 7— not a rule built into the language.
0.3.69
- A definition's number now counts. When a line could be grouped more than one way, a looser definition is never read as sitting inside a tighter one, and two at the same declared number group left to right. Nothing in the prelude declares a number yet, so existing programs read as before; when a line has to be read from a later word, the reader now takes one definition there and reads the rest from the start again, rather than carrying on from it; your own definitions can, e.g.
define (a: Scalar) times (b: Scalar) => Scalar at 7:.
0.3.68
- A definition can say how tightly it binds, after its return type:
define (a: Scalar) + (b: Scalar) => Scalar at 6:. One that gives back nothing writes=> Null at 1. Nothing uses the number yet — it is the first step of trying out precedence.
0.3.67
- The classic syntax is gone. There is one Tilde now: the one that reads its words from definitions. The playground no longer has a syntax picker, and its starting example has been rewritten (the old one no longer ran).
- The docs are rewritten for it. Getting Started, Elements, Constraints, Settings, the Language Reference and the Certainty page now use the language as it is, with every example checked to run; the Pick and Unit Resolution pages are gone.
- Gone with it, for now:
pick, units (set unit,5cm,60deg),set winding, angles, lines by equation (line l = (m, k)), a point on a segment, part-placed points ((5,)), named segments,triangle abcname splitting andt_1/t_1_2subscripts, circle diameter,with … and …lists, comma lists and;. Each is listed on the roadmap with what it needs to come back.
0.3.66
- The prelude no longer defines
point p at x y,point p on l,circle c with center p,circle c with radius r,line l parallel morline l perpendicular m. They all still work, exactly as before: each reads aspoint p(orline l,circle c) and then what is said about it. Your own definition of any of them now wins simply by being longer. - A line that cannot be read now says why from as far as it got:
point p on q, withqunknown, reports that noonfitspoint p on q, listing the ones it tried.
0.3.65
- Where something lands decides what it is. Two definitions may share their words and differ only in what they give back — a pair of numbers as a point or as a line. In a point's slot it is the point, in a line's slot the line, with or without brackets:
l parallel (2, 1)andl parallel 2 , 1both use the line. - Where nothing says which — the pair alone, or in a slot that takes anything, like
print— the line is refused with… could give a Line or a Point, and nothing around it says which, and asked to name it first where that would help.
0.3.64
=takes everything after it.scalar k = 2 + 3makesk5, with no brackets:=gives nothing back, so the line is read with2 + 3worked out first. Likewisepoint p = (3, 4) on lnames the point and puts it onl.a = b = cis refused — neither half gives anything back to chain. Write it as two lines.
0.3.63
- The editor colours a chained line all the way through.
line l parallel m through pmarksl,mandpas values, not just the first part. Symbols in patterns (=,+) count as pattern words, and what is inside brackets is coloured the same way. - New docs page, Reading a Line: how a statement in the definition syntax is read, left to right.
0.3.62
- A line is read left to right. The longest definition that matches from the start is taken, and what it gives fills the first slot of whatever comes next. So statements chain:
line l parallel m through pis((line l) parallel m) through p, andl parallel m through pworks on lines that already exist. - A longer definition wins. Write your own
define line (n: new Line) parallel (m: Line)andline l parallel muses it, rather thanline land then… parallel m. If yours does not fit the types — say it takes a Circle — the shorter reading is used instead. - When nothing reads from the start, a later word is tried.
print 1 + 2prints 3:print 1cannot be followed by+ 2, so1 + 2is read first and printed. This is also how a definition that takes nothing is used:print greeting. - Arithmetic reads left to right until it has precedence:
2 + 3 * 4is 20, no longer an error asking for brackets. scalar r = 2 + 3now reads as(scalar r = 2) + 3and is refused for adding to a named number. That changes once=stops handing back what it names.
0.3.61
- The same words can be defined with different return types.
define pair (x: Scalar) (y: Scalar) => Pointand… => Scalarare two definitions rather than one defined twice. Standing alone, a line that fits both is ambiguous; choosing by where it is used comes later. - Shadowing follows suit: your own
… => Pointreplaces an imported… => Point, but not an imported… => Line. - "Defined twice" errors now show the return type:
defines `twice «Name» => Point` twice.
0.3.60
- What a bracket works out stays a value.
line l through (1,2) (3,4)puts the two points straight into the line's slots instead of writing them back into the line as text and reading it again. Nothing reads differently; errors now show what was written —"l parallel (1 , 2)"— rather than an internal name for the point. #is no longer a character a name can contain:point Point#1now stops at the#withunexpected character '#'.
0.3.59
- Every type has to exist. A type written in a definition — a slot, a
=> Type, or a field ofdefine type— must be one of the built-ins or come from adefine type. Otherwise it is an error on that line, with a suggestion when only the capitals are wrong:there is no type called "point" — did you mean "Point"? - Two points can be said to be the same:
n.a = p. For now the two are held at no distance apart — they stay two points, drawn on top of each other — until=can merge them into one.
0.3.58
- Fixed a misleading error: a name that happened to match a slot of a definition that had already run (
xincall (x: Any) …) was reported as "local to" that definition. Now the real problem is reported.
0.3.57
(ty: Type): a slot that takes the name of a type — a built-in (Point,Line,Circle,Scalar) or anydefine type.newuses it, sonew Triangel tnow saysthere is no type called "Triangel"on that line.
0.3.56
- Outputs in definitions are written
new:define point (n: new Point) at (x: Scalar) (y: Scalar). The caller writes a name there and gets back, under that name, whatever the definition made — or chose, since it can name something that already exists (point p = (3, 4),triangle t with a b c). - If a definition never makes one of its outputs, it gets a fresh one of the promised type. If it makes the wrong kind of thing, that is an error naming the definition.
- A name that is already taken is refused at the line that tried to reuse it.
(s: Name)now means just a word — a setting's name, or a type's — and is passed into a definition's body as itself.
0.3.55
- A definition's own names are its own. Inside a body, any word can name something — including words that are also part of a pattern, like
set,on,originorgrid. Before, those were quietly treated as pattern words and could not be used. - A body sees only its slots and the names it makes itself, never the program's names — as before, now for a clearer reason.
- Using a definition's own name from outside says so:
c with radius 9afterdot d at 3 4reports thatcwas local todotand ended when it returned. - Printing something nothing names shows the language's own key for it, like
Point#1 = (1, 2). @and#cannot appear in names; the language uses them for its own bookkeeping.
0.3.54
- In a
tsxbody,declare(name, type)now always makes an element's fields from itsdefine type— a Triangle comes with its three points. Fields can no longer be handed in (declare(n, 'Segment', { from: p, to: q })); to tie a field to a name, make the element and name its fields, astriangle t with a b cdoes:new Segment s,call s.from p.mintis gone —declaredoes what it did.
0.3.53
set grid,set axesandset origin, in the definition syntax, eachonoroff. The axes are now separate from the grid, so either can be shown alone.set origin onmarks (0, 0) with a dot labelled O. Defaults: grid and axes on, origin off.setwith a setting that does not exist says which ones do.- Setting names stay free to use as names:
point origin at 0 0still works, inside a definition too.
0.3.52
- Subscripts, in the definition syntax: the part of a name after
_is drawn as a subscript, on the canvas, in tooltips and in printed output.t_1shows as t₁,t_1_2as t₁,₂,l_1'as l₁', andt_1.point_2as t₁.point₂. On by default;set subscripts offturns it off. The setting can go anywhere in the program; setting it both on and off is an error. - The classic syntax now draws its subscript names (
t_1) as subscripts too, always. - In the definition syntax, a name can no longer start or end with
_, or have two in a row. Names starting with_belong to the language's own hidden shapes, so a program could otherwise reach or clash with them.
0.3.51
- Withdrawn: brackets around a single word that names nothing no longer run it as a statement. Brackets around one word always mean just that word, as before 0.3.50.
0.3.50
- Text:
"hello", with\",\\,\nand\tas escapes.print "hello"prints it, and+joins two pieces of text:print ("hello, " + "world"). Text only joins to text — a number is not quietly turned into text. - Text stays distinct from a name spelled the same way:
print pprints the pointp,print "p"prints the letter. - A single word in brackets that names nothing now runs as a statement, so
(greeting)calls a definition that takes no arguments. Brackets around a name that exists still change nothing. - Roadmap: working with solved values — a stage that runs after the solver, where printing a point could be written in terms of printing its numbers, and measurements could give back a value.
0.3.49
print, in the definition syntax:print rshows a value in the playground console, above the status line. It reports the solved value — so a number worked out from the geometry prints its answer, and when there are several possibilities, none, or nothing yet pins it down, it says so. Works on points, lines and circles as well as numbers:q = (5, 0) or (-5, 0),l: y = 0.
0.3.48
- Arithmetic on written numbers:
scalar r = ((2 + 3) * 4)is 20, and a sum or product can go anywhere a number can —point p at (1 + 1) (10 / 5). The four operators are ordinary definitions in the prelude rather than built into the language. - Negative numbers:
-3,(1, -2),at -3 4. A minus touching a value on its left subtracts (a - 3,a-3); one with nothing value-like before it is a sign. - Not yet: arithmetic on a named number (
k + 1), which is refused rather than guessed, and precedence —2 + 3 * 4needs brackets.
0.3.47
- Names can carry primes:
l',A''. Tilde has no reassignment — giving a name a second, different value is a contradiction — so something made from an existing shape gets a new name, the way geometry is written on paper: rotateland call the resultl'. - Roadmap: explicit redefinition, for the rare case where you really mean to replace a value, as a possibility.
0.3.46
- Roadmap: an open question on what
=should mean — whether naming something and saying two things are the same should be one feature or two.
0.3.45
- Names inside a definition are private to it. A name a definition uses for its own working parts ends when the definition finishes, the way a local variable does in most languages. The shapes it named are kept — the drawing may depend on them — but nothing outside can refer to them. To hand something back, a definition returns it; to expose a part permanently, make it a field of the type.
- A definition's helper shapes are no longer drawn. Previously they appeared on the canvas under a generated name. Shapes built under the name the caller supplied, and fields of a declared type, are unaffected.
- Referring to a finished definition's private name now says so, rather than reporting that nothing matches.
0.3.44
- Brackets group.
line l through (1,2) (3,4)andcircle c with center (0,0) and radius 2work: a bracketed group is worked out first and its result fills the slot it sits in, wherever that is. Groups can sit inside groups, and brackets around a single name or number change nothing. point p = (3, 4), and a pair of numbers anywhere a point is expected. A pair is an ordinary definition in the prelude rather than something built into the language, and brackets are required when it is used — not because the definition says so, but because a slot takes one word, one number or one bracketed group.- A definition that makes something of its own no longer needs to be given a name. Each use gets its own copy either way; previously one with nothing to name it by was refused.
- Mismatched brackets are reported where they are — a bracket never closed, one that closes nothing, or an empty pair.
0.3.43
- The pair syntax from 0.3.43 (
point p = (3, 4)) has been withdrawn. It worked by treating brackets and commas as ordinary words in a pattern, which meant every place a pair could appear needed its own definition. Brackets are better used for grouping — so that(3, 4)is a point wherever one is expected, written once — and that is being designed rather than approximated.
0.3.42
- Roadmap additions: a true/false type with a third "not known yet" value; the ability to ask whether something holds as well as assert it, using the same words for both (
is a parallel b); and arithmetic, with a note on what makes solving it harder than writing it.
0.3.41
- A Limitations page, listing what Tilde does not do yet and — more usefully — what it does quietly: constraints that are read and then ignored, answers chosen for you where your program was silent, and contradictions that leave a shape out rather than saying so. The aim is that you can tell a bug from a gap.
- The Roadmap covers the definition syntax now, and two entries claiming work was unfinished have been removed because it since got done.
0.3.40
=can be written where it reads naturally:point p = 3 5,scalar r = 2,a = 0 0. It is not a built-in operator — it is a character a definition may use in its pattern, likeatoron, and it means whatever that definition does.atstill works exactly as before, and a program can give=its own meaning for its own shapes.=reads as equality, never as assignment.r = 2followed byr = 3does not changerto 3 — it says the number is both, which it cannot be, soris left with no possible value. This was also a real bug: giving a number two different values used to keep the second one silently.A definition's line now ends with
:instead of=, which is what frees=for use inside patterns:define point (n: Name) = (x: Scalar) (y: Scalar) => Point: point n at x y return n
0.3.39
- Two named numbers can be made the same number:
scalar h; h is wsayshandware one number, and it does not matter which becomes known first. Neither is copied into the other — each is narrowed to what both could be, so the answer arrives from whichever direction it happens to come. - "Nothing known" and "no possible answer" are now different answers. They used to be indistinguishable, both reported as an empty list. A shape nothing pins down could be anything; one that has been asked for the impossible could be nothing. Telling them apart is what lets two numbers be combined properly: the first constrains nothing, the second rules out everything.
- An impossible number no longer stops the drawing.
scalar a is 7followed byscalar b is 3anda is bmarks both as having no possible answer and leaves the rest of the figure alone, rather than abandoning the whole thing.
0.3.38
- A way to see what your drawing actually pins down. The strategy picker in the playground gains a
noneoption, which places only what the constraints force and leaves everything else out. A drawing has to start somewhere, and when a shape is genuinely underdetermined something has to choose a representative — both are choices your program did not make, and this shows you where they happen.point a at 0 0followed bypoint band a line through them draws onlyaundernone: the pointband the line were being chosen for you.
0.3.37
- Fixed: a definition could not reach a part of the shape it was given. Writing
n.p at x yinside a definition, wherenis one of its slots, failed with "n is not declared". The name and the part-of-a-name were being treated as unrelated, so the slot was never filled in. This affectedtriangletoo —triangle t with a b chappened to work whiletriangle q with a b cdid not, because the definition's own slot is calledt.
0.3.36
- A named number reports every possible answer, not just the first. Where a shape has more than one possible position, a number read off it has more than one possible value — a point where a circle meets a line sits at x = 5 or x = −5. Previously such a number quietly reported 5, as though a choice had been made. It now reports both, stays in step with the shape answer-for-answer, and can be chosen between with
picklike anything else.
0.3.35
Numbers can have names.
scalar r is 3names a number;scalar ron its own says only that some number is there, and leaves the solver to work out which.scalar r is 3 point o at 0 0 circle c with center o c with radius r circle d with center o d with radius rBoth circles come out the same size, and if you change
3they both follow. Neither circle was given a number of its own —ris what makes them agree.A named number and the shape it describes settle each other, in whichever order the answer arrives. Give the number and the shape takes it; pin the shape some other way and the number follows. Previously only the second direction worked — a named size was quietly ignored, and the shape drifted to wherever it would have gone anyway.
Anywhere a number can be written, a named one can go instead:
point p at k j,point p at k 2,circle c with radius r. Mixing the two in one statement is fine.
0.3.34
Shapes are made and named with three general-purpose words, replacing the triangle-specific
holds.newmakes a shape of any kind, reaching down through whatever parts that kind is declared to have;callgives a second name to something that already exists;segmentdraws an edge between two points. A triangle is now written entirely in Tilde, with no escape into the implementation:define type Triangle = Point point1 Point point2 Point point3 define triangle (t: Name) with (a: Name) (b: Name) (c: Name) => Triangle = new Triangle t call t.point1 a call t.point2 b call t.point3 c segment a b segment b c segment c a return tAnyone can write a shape this way — nothing here is reserved for the built-in ones. Corners are numbered rather than lettered so the naming carries to shapes with more of them.
A name given by
calland the thing it names are one and the same. Aftertriangle t with a b c,aandt.point1are two names for one point: constrain either and you have constrained both. It is not a copy kept in step, so you can mix the two freely.Triangle moved out of the core file, since the solver knows nothing about it — it is three points and the edges between them, so it belongs with the other shapes.
Drawings label what you wrote. A triangle's corners are stored under the triangle, but if you called them
a,bandc, that is what appears on the canvas, and its edges readab,bc,acas they always have.A slot can be written
Anywhen a definition works on a shape without caring what kind it is —callis the first to need it. Use it sparingly: a slot that accepts everything makes it easier to write two definitions that both match the same statement.
0.3.33
Types can declare what they contain: a shape can now say what parts it is made of, and those parts can be reached by name.
define type Triangle = Point a Point b Point cFields are written type-then-name —
Point a— the same order a statement declares a point in (point a), so the two read alike. With that,triangle t with a b cgives yout.a,t.b,t.calongside the names you chose, and they are the same points either way:t.a at 0 0anda at 0 0do the same thing.Dotted names work anywhere a name does.
line l through t.a t.b,t.a on l,distance between t.a and t.b is 5. The kind of thing dispatch sees is the field's kind, not the shape's, sot.a on lpicks the point-on-line meaning without anything special.You can declare your own types, not just use the built-in ones — a
Segmentwith afromand ato, say — and reach their parts the same way.A field always holds a whole shape: a point, a line, a circle. It cannot hold a piece of one, so there is no
Scalar xinside a Point. This is deliberate — a line found by two tangency conditions has two possible answers, and those answers live in the line as a whole; splitting it into three separate numbers would turn two real answers into eight false ones.Mistakes are reported plainly:
Triangle has no field "z" (it has a, b, c),Point has no fields, so "p.a" means nothing, and setting a field to the wrong kind of shape names both kinds.
0.3.32
returnsays what a definition gives back: a definition's body ends withreturn, naming which of the things it built comes back. Previously the last line's value was used, which meant the result depended on the order you happened to write the body in. Now order is free:define dot (n: Name) at (x: Scalar) (y: Scalar) => Circle = point n at x y circle c with center n and radius 1 return cA definition that states
=> Circlemust have areturn, and one that returns must state a type — mismatches are reported rather than silently handing back the wrong kind of thing.returnmay only be the last line: it says which value comes back, it does not skip anything. It joinsdefine,import,exportandtsxas the only words that cannot be redefined.A definition can be used more than once: names a body writes on its own account now belong to the call rather than the program. Calling
dot d at 3 4and thendot e at 1 1used to fail with "c is already declared" — thecinside was one shared name. Each call now gets its own, keyed by the name you passed:d_c,e_c. Names that came from the call itself (d,e) are untouched. Definitions that name nothing of their own — which is all of the prelude — behave exactly as before.A definition that names something of its own but takes no
Nameslot to key it by is reported rather than silently colliding.
0.3.31
Definition syntax in the playground: a
syntax:picker in the header switches the editor betweenclassic(unchanged, still the default) anddefinitions, a new front end where the language's own constructs are written in Tilde rather than built into the parser. Indefinitionsmode a program starts withimport prelude, andpoint,line,circle,parallel,onand the rest come from prelude files instead of the lexer — so you can write your own shapes and use them in the next line:import prelude define dot (n: Name) at (x: Scalar) (y: Scalar) => Circle = point n at x y circle c with center n and radius 0.001 dot d at 3 4Each mode keeps its own saved buffer, so switching never overwrites the other.
Highlighting follows meaning, not spelling: in
definitionsmode only four words are keywords —define,import,export,tsx— because everything else is defined in the prelude and can be redefined by anyone. The rest of the colouring comes from matching each line against the definitions in scope, so the values filling slots are told apart from the words around them. The same word can be either:line l through p qcolourslineas a pattern word,p on linecolours it as a value. Your own definitions feed the same table, so a shape you invent highlights like a built-in one.Optional semicolons: a line may end with
;anywhere a line ends — statements, imports, and the lines inside a definition. Never required, and a program written with them means exactly the same as one written without. Not allowed on adefineline, which ends in=with its body still to come.Known limits in
definitionsmode: scalars,pick,set unit,set grid, point literals like(2, 1), and bracketed sub-expressions are not available yet. A definition that names something in its body (rather than taking it from a slot) can only be used once — using it twice reports that name as already declared.
0.3.30
- Docs site home page and styling: retitled from "A geometric programming language" to "A geometry definition language" — Tilde is declarative, with no control flow, so "programming language" was misleading (and "geometric programming" is an unrelated term from convex optimisation). The old tagline is gone. The home page picks up a subtle graph-paper grid backdrop, and horizontal dividers in the docs (plus the sidebar group separators) render as dash-dot patterns instead of solid lines.
0.3.29
with diametersyntax for circles:circle c with diameter 6is sugar forwith radius 3. Combinable withcenter(circle c with center p and diameter 8) and the bundled-scalar form (circle c with diameter d = 6declaresscalar d = 6— the diameter — and sets the circle's radius to 3). Only literal numbers are accepted today; for scalar-referenced or solver-derived sizes usewith radius.
0.3.28
- Circle as a point-on-locus stand-in:
c on lnow means "c's centre is on line l", and the symmetricl through cmeans "l passes through c's centre". The same applies to segments (c on ab). Useful for classical compass-and-straightedge constructions where the centre's locus is constrained — inscribed circles, tangent circles, Apollonius problems. Two circles can't relate this way:c1 on c2andc1 through c2are now rejected at elaboration (previously they parsed silently with no geometric meaning). - Stricter naming checks: using a circle name in a point position (e.g.
c = (3, 4)whencis a circle) is now an elaboration error rather than silently creating a phantom point. On and through — the matrixdoc section: the constraints docs gain a matrix listing every LHS / RHS element-type pair and what it means (or that it's rejected). The on-circle section also documents thethroughform (circle c through p).
0.3.27
- Circle hover tooltip: hovering over a circle in the playground now shows its label, centre, and radius instead of the stale tooltip text from the previously-hovered element. The hover handler also gains a fallback branch that shows the element's label (or
(no format)) for any kind that doesn't have a custom formatter yet, so similar bugs won't repeat as new shapes are added.
0.3.26
- Underdetermined circles render through their on-circle points: a bare
circle cwith one or two placed points on it (e.g.point p = (1, 2); circle c; p on c) used to throw because the default radius of 1 conflicted with the constraint. The circle now places its anonymous centre canonically — at the origin for a single on-circle point, at the chord midpoint for two — and lets the radius be derived from there. The circle renders as wavy (its centre is a representative choice). Three or more on-circle points continue to determine the circle exactly via the circumcentre rule.
0.3.25
- Multi-target
through:l through p, qandl through p and q(with comma orandseparator) now work as standalone constraint statements, not just inline on a line declaration. Each target desugars to a separate on-line constraint. The same syntax also works for circles:c through a, b, c. throughclause on circle declarations:circle k = (o, 1) through a, b, cworks inline, mirroring the existingline l = (1,) through p, qform. With three placed points the circumcentre rule completes the circle.- Chaining constraint operators is now rejected:
l through p parallel mused to be silently accepted in line declarations (treating it asl through p; l parallel m) but was actually a parse error in standalone constraint statements. The reading is ambiguous (ispparallel tom?), so it's now a parse error everywhere. Each constraint clause needs its own statement. - Shared constraint-body parser: standalone constraint statements (
l through p), line-decl trailing clauses (line l = (1,) through p), and circle-decl trailing clauses now go through one shared parser. Future constraint operators added there automatically work in all three contexts.
0.3.24
- Circles: first-class element declared as
circle c = (p, 3)(named centre + radius),circle c = ((x, y), r)(inline centre), orcircle c(bare — centre at origin, radius 1). with centerandwith radius: alternative property syntax —circle c with center p and radius 3. Each parameter can be specified independently;=is optional.- Bundled forms for circles:
circle c with center p = (5, 3)declares pointpand places it;circle c with radius r = 4declares scalarrwith value 4. - On-circle constraints:
p on cconstrains a point to a circle. Combined with another locus (line or second circle), a point can be placed at the exact intersection. - Three points determine a circle: if three or more placed points lie on the same circle, the solver derives its centre and radius.
- Derived circle radius: a scalar declared without a value and used as a circle's radius is back-propagated when the radius is determined by an on-circle constraint.
0.3.23
- Partial point declarations:
point p = (5,)declares a point withx = 5andyunknown;point p = (,3)declares the symmetricy = 3, xunknown form. The two halves can be combined across multiple statements —point p = (5,)followed bypoint p = (,3)merges to a fully placed point at(5, 3). The syntax mirrors the existing partial-line formsline l = (m,)andline l = (,b). Internally a partial point is treated as a point lying on an axis-aligned line, so existing constraints work without special cases: e.g.point p = (5,); line l = (3, 2); p on lresolves top = (5, 17)automatically.
0.3.22
- Unified solver architecture: the old anchor-then-resolve two-pass solver has been replaced with a single loop that alternates between propagate (apply every forced placement) and pick (place one element by canonical gauge fixing or by a representative choice). Behaviour is identical — the rule-based strategy still passes the same 88 tests — but the internals are cleaner and easier to extend. The playground dropdown now reads
pick: rule/pick: budgetinstead ofanchor: rule/anchor: budget. The solver documentation has been rewritten to match the new structure.
0.3.21
- Fix dof reporting for free 1-locus points: a point with a known distance to a single placed neighbour (and no other constraints) used to be reported as fully determined when it happened to be placed first, even when the neighbour itself was at an arbitrary position. It is now correctly reported as underconstrained — the placement we picked is representative, not canonical. Affects scenes with disconnected components whose canonicalization can't be reached from the anchor (e.g.
point a; segment bc = 5).
0.3.20
- Better anchor accounting for partial lines with a free point:
line l = (1,); point p on l(and the symmetric direction-only / y-intercept-only forms) now resolves fully — the T-gauge that was previously unused is consumed by placing the point at the line's natural position (origin for slope-only and direction-only forms; the line's pinned point for y-intercept-only forms). Both line and point render as solid instead of underconstrained. - R-gauge tracking when canonicalizing line direction: when a connected line gets a canonical default slope filled in (e.g. y-intercept-only line with a free point at the y-intercept), the line is marked as resolved (dof=0) if rotation-gauge was still available, rather than left as underconstrained.
0.3.19
- Optional
=inwithclauses:line l with slope 2 and intercept 1now parses the same asline l with slope=2 and intercept=1. The=between a property name and its value is optional; existing code is unchanged. - Bundled scalar declarations in
withclauses:line l with slope m = 2is sugar forscalar m = 2; line l with slope=m— declares a named scalar and uses it as the slope in one statement. Double-=forms likewith slope = m = 2are rejected to enforce one binding per name.
0.3.18
- Solver-derived scalars: scalars can now be declared without a value (
scalar m) and derived by the solver from geometric constraints. For example,scalar m; line l = (m, -1, 0); l through (0,0); l through (1,3)determinesm = 3. Multiple scalars can be derived from the same element. - Bare scalar declarations:
scalar m(no= value) declares an unknown scalar whose value is determined by the solver.
0.3.17
- Scalar declarations:
scalar m = 3declares a named constant usable anywhere a number is expected — coordinates, line equations, length constraints, inline tuples. Scalars support forward references and can reference other scalars. Scalartype: geometry primitives (Point,Line) are now defined in terms ofScalar(=number), documenting the relationship to the language concept.
0.3.16
- Inline tuple refs: numeric tuples can now appear wherever a name is expected —
line l perpendicular m at (1, -1)places the intersection inline,line l parallel (1, -1, 0)references a line by equation,p on (1, -1, 0)constrains a point to an inline line. Optionalpoint/linekeyword disambiguates when the tuple length is ambiguous for the context. - Templatized element declarations:
LineDeclandPointDeclnow share anElementDecl<K, T>template with aparams: Nullable<T>field, ready for future element types (e.g. circles). - Declarations no longer carry constraints: inline sugar (
through,parallel,perpendicular,= 5) is expanded by the parser into separate constraint statements. Declarations are pure declarations.
0.3.15
- Line anchoring: disconnected bare lines now correctly render as underconstrained when other elements (e.g. fixed points) consume the global symmetries that would otherwise absorb the line's degrees of freedom. Previously, a bare line always rendered as fully determined regardless of surrounding constraints.
0.3.14
- Solver interface: introduced a shared
Solverinterface (ConstraintSet→SolveResult) enabling swappable solver backends. - Elaboration layer: new
elaborate.tstransforms the AST into a solver-agnosticConstraintSet, separating semantic analysis (ref resolution, unit conversion, shape expansion) from solving. - Geometric solver encapsulated: existing solver code moved into
solver/geometric/with a single public entry point (GeometricSolver). - Shared geometry types:
PointandLinedefined once in the solver interface, used everywhere. - Removed
freeCoefs: line tooltips now always show concrete coefficients.
0.3.13
- Line rendering by certainty: underconstrained lines now render with a squiggly stroke; two-solution lines (e.g.
l parallel m at 3) render with a jagged stroke. Both use the same colour coding as points and segments. - Line labels coloured by certainty: label colour now matches the line's constraint state.
pickworks for lines:pick l 1/pick l 2selects one solution from a two-solution line, rendering it as fully resolved.- Line labels: always visible, placed inset from the viewport edge with a small perpendicular offset so they stay inside the canvas at all orientations.
- Line hover: hovering over a line now shows a tooltip with its name and constraint state.
0.3.12
- Parallel lines:
line l parallel mconstrainslto have the same direction asm. Can appear inline on thelinedeclaration or as a standalone statement. Optionallinehints accepted on both sides (line l parallel line m). - Perpendicular lines:
line l perpendicular msetsl's direction perpendicular tom. - Intersection point shorthand:
line l perpendicular m at pdeclaresl, marks it perpendicular tom, and places pointpat their intersection — sugar for addingpto both lines'throughlists. - Parallel distance:
line l parallel m at 3constrainslto be exactly 3 units fromm, producing two symmetric solutions (one on each side).
0.3.11
- Partial line declarations: a line can now be declared with one parameter unknown —
(m,)for slope-only,(, k)for y-intercept-only,(a, b,)for direction-only. If a placed point lies on the line the missing parameter is solved exactly; otherwise a canonical default is used. A line resolved by default is rendered as underconstrained (like a free point), one resolved by constraint is fully crisp.
0.3.10
- Anchor bug fix: a free point with a length constraint to an already-fixed point is no longer selected as the translation anchor — anchoring it at the origin would violate the distance constraint
0.3.9
- Test suite: Vitest added;
src/tests/solver.test.tscovers bare segment/triangle, subscript triangle, length constraints, explicit point placement, contradictory-position errors, and line-intersection placement
0.3.8
- Subscript shape support: shapes in subscript mode (
triangle t,segment s) now register vertices (t_1,t_2,t_3) and edges in the solver - Subscript segment syntax:
t_1_2(double underscore) is the canonical edge ref for subscript shapes — unambiguous regardless of vertex count - Unified ref system: parser now produces only
NameRef | SubscriptRef; all semantic resolution (line vs segment vs vertex) moved to the solver where the symbol table is available p on t_1_2works: on-constraints now accept subscript segment targetspoint aaftersegment ab: re-declaring an implicitly created vertex is now allowed — coordinates are applied as a position constraint rather than throwing- Internal:
MeasureConstraintsplit intoLengthConstraintandAngleConstraint;PointCoincidencemerged intoEqualityConstraint
0.3.7
- Any-case identifiers: names can now be any mix of upper and lowercase (
MyTriangle,Seg1,hello) — the previous all-upper / all-lower restriction is removed - Flexible shape naming: exact-length all-distinct lowercase names decompose into vertices (
triangle abc→ verticesa,b,c); any other name uses subscript mode (triangle t→t_1,t_2,t_3) - Repeated characters in shape names route to subscript mode instead of erroring (
segment ss→ss_1,ss_2) - Position constraint inline:
a = (1, 2)in awithclause or as a standalone statement places a vertex at exact coordinates; errors if already placed at a different position - Language reference: new
/referencepage with complete syntax listing, marking unimplemented features
0.3.6
- Line ∩ Line constraint: a point constrained to two named lines is placed at their intersection (
point p on a; p on b) - Constraining a point to three or more lines checks that all lines share a common point, throwing a constraint error if not
- Bare point declaration:
let point anow works without coordinates — declares a free vertex the solver places normally - Bare line declaration:
let line lnow works without an equation — defaults toy = x
0.3.5
- Docs: expanded solver internals into five pages — overview, unit resolution, constraint model, anchor, and placement loop
- Docs: introduced locus-intersection model as the unifying framing for the placement algorithm
- Docs: added Mermaid flow diagrams throughout solver internals
0.3.4
- Docs: fixed incorrect example in certainty page (underconstrained example now correctly shows a point on a circle, not a fully free point)
- Docs: removed internal terminology (
infinite,one,heading,P2) from user-facing pages - Docs: fixed zoom levels on pick and certainty examples
0.3.3
- Length constraint
ab = 5now implicitly declares and renders the segment - Segments where both endpoints are explicitly placed render crisp (not wavy)
- Docs: settings page updated with unit documentation, stale entries removed
- Added
CLAUDE.mdwith project conventions
0.3.2
- Length units:
set unit cm(ormm,m,in,inches) sets the default unit for the program - Mixed units convert automatically —
50mmand5cmare the same length whenset unit cm - Without
set unit, the first unit used in any constraint becomes the default; fully unitless programs stay abstract setstatements are now validated to appear before any geometry declarations- Removed
set anchor(use explicit point coordinates instead)
0.3.1
- Fixed isolated disconnected components (e.g. two unconnected segments) stacking both vertices at the same point — each component now seeds separately so constraint propagation resolves it correctly
0.3.0
- Multiple solutions: ambiguous placements now render all discrete solutions simultaneously as jagged numbered alternatives
pick v Nstatement to select a specific solution- On-segment constraint:
p on abplaces a point on a segment (alwaysinfinite) - Multiple underconstrained points on the same segment distribute evenly
solutionsmodel replaces oldcertaintymodel:one | multiple | infinite- Jagged line and circle style for
multiplestate (amber)
0.2.0
- Equation-defined lines:
line l = (a, b, c)orline l = (m, k) - On-line constraint:
b on l,point b on line l,b on line l - Explicit point declaration:
point p = (x, y) - Circle-line intersection solver (P1b priority)
- Name collision detection — reusing a name across shapes/lines/points is a runtime error
- Vertex-centric fixpoint solver replaces old per-shape placement
- Rotating heading (90° CCW per P2 placement) prevents collinear degeneracy in underconstrained cycles
0.1.0
- Initial language:
segment,triangle - Length constraints:
ab = 5 - Point coincidence:
a = b - Two-circle intersection solver (SSS triangles)
set grid on/off- Canvas2D renderer with pan, zoom, resize
- Wavy circle / squiggly line for underconstrained (
infinite) vertices - Semicolons as optional statement terminators
- LocalStorage persistence in playground