# SVG Sketch Source

> Source: <https://tomlarkworthy.github.io/lopebooks/notebooks/tomlarkworthy_svg-lens.html#view=S100(@tomlarkworthy/svg-lens)>
> Published: 2026-07-27 21:14:54+00:00

The drawing syncs to the code and vice versa. Switch between using code and UI. Built on composable lens [[Foster et al. 2007]](#) and edit-lens [[Hofmann et al. 2012]](#) theory, but applied in the string domain so unknown semantics like SVG animations are preserved without parsing. Follows the path of Sketch-n-Sketch [[Chugh et al. 2016]](#) [[Hempel et al. 2019]](#) but applied to an existing image format: SVG. Generated with a novel prompting strategy of requesting an LLM to use property checking against mathematical laws as a architecture, leading to strong implementation consistency over an expansive domain.

| gesture | what it does | lens |
|---|---|---|
| drag a shape | moves it | `transform` |
| tap a polygon or path, drag a handle | moves one vertex or anchor | `points` , path `d` |
| tap anything else, drag a corner or the rotate handle | scale or rotate from the box | `transform` |
| drag the pivot dot | moves the point the box scales and rotates around | `transform` |
| double-click an edge / a vertex / empty canvas | add a vertex / remove one / drop in a shape | `children` |
| tap again in the same place | steps down through stacked shapes | — |
| shift-tap, or rubber-band | adds to the selection | — |

A selection also carries **chips**: the same commands, one tap away, drawn only when they apply:

| chip | chip | ||
|---|---|---|---|
⧉ |
duplicate | ∿ |
smooth ↔ corner the anchor |
⇄ |
swap fill and stroke | ◑ |
give it a gradient (minted into `defs` ) |
≡ (drag) |
stroke width | ➤ |
put an arrowhead on it |
⊚ / ◠ |
close / open the path |

The panel under the drawing edits `fill`

, `stroke`

, width, opacity and the dash as typed inputs;
each change commits through the same write path, so the source keeps the notation you wrote.

| key | |
|---|---|
R E L |
drag out a rect, an ellipse, a line |
P |
place path anchors; click the first again to close, double-click to finish open |
S |
scribble a freehand path; it is fitted to Béziers on release |
V / Esc |
back to selecting (a finished tool returns by itself) |
⌘G / ⇧⌘G |
group / ungroup the selection |
⌘D |
duplicate |
⌘C ⌘X ⌘V / ⇧⌘V |
copy, cut, paste / paste in place |
⌘A |
select all |
1 / 2 |
reset the view / fit it to the selection |
[ ] / { } |
lower, raise / send to back, front |
Delete |
removes the selection |
⌘Z / ⇧⌘Z |
undo, redo (byte for byte), and declines if something else wrote to the cell first |
arrows, shift+arrows |
nudge by one unit, by ten |
alt while dragging |
ignore snapping |

```
import {svgLens} from "@tomlarkworthy/svg-lens"
viewof picture = svgLens(svg`<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="20" fill="tomato"/>
</svg>`)
```

*What follows is the reasoning behind the editor above: what a lens is, which laws it keeps, how we combine to cover a complex domain like SVG, how the maths of sync is kept apart from the UX, and additionally how to apply when the drawing is itself a parameterized function.*

You can create computer graphics by typing code or by a graphical user interface, each has advantages in certain situations. If we can switch between these two modes fluidly, bidirectional, the advantage of each can be leveraged at that moment in time and the combined sum is greater than the parts in isolation. Keeping both means keeping them in step, which is the view-update problem [[Bancilhon & Spyratos 1981]](#) recast for programs [[Chugh et al. 2016]](#). Feedback in the other direction, from program to picture, is the older half of the problem and has a standard vocabulary [[Tanimoto 1990]](#); what is at issue here is the return path.

Propagation is the hard part. Sketch-n-Sketch infers program changes from output changes by tracing values back to the expressions that produced them [[Chugh et al. 2016]](#) [[Hempel et al. 2019]](#), which works for programs the tracer understands.

This notebook takes a different approach. The update path is a composition of bidirectional lenses [[Foster et al. 2007]](#), which gives it laws, a stated domain, and property tests ([ §2.1 The laws](#)).

What is editable is a drawing whose shape is written literally; interpolated values are rendered but left to their source ([ §7 Interpolation and holes](#)).

A **lens** between a source type S and a view type A is a pair of functions [[Foster et al. 2007]](#):

and that is exactly the chain a drag runs down: cell source → template literal → attribute string → typed value, then back up again. Composition of well-behaved lenses is well-behaved, so the laws are established once, per level, and hold for the composite.

Two laws make a lens *well-behaved* [[Foster et al. 2007]](#):

**GetPut** (left) says a null edit is a null write: drag a shape back to where it started and the cell
is byte-identical to before, not merely equivalent. **PutGet** (right) says the view you asked for is
the view you get, so a drag of 40 pixels moves the shape 40 pixels and the number in the source
agrees.

A third law, **PutPut**, makes a lens *very* well-behaved:

That is, the last write wins and intermediate ones leave no trace. Most of these lenses keep it; some
deliberately do not ([ §2.2 Residue, and why PutPut bends](#)).

SVG's syntax is not canonical. `"0,0 100 100"`

and `"0 0 100 100"`

are the same rectangle;
`rotate(45)`

and `matrix(0.707 0.707 -0.707 0.707 0 0)`

are the same transform. So get
is not injective: a whole *fibre* of source strings maps to one view.

Any put has to pick a representative from that fibre. Reprinting canonically is the obvious choice, and it reformats the file every time a shape is nudged. The rule used here instead:

Skip rule.If a=get(s), return s unchanged.

This implements GetPut rather than merely satisfying it, and it is what keeps comments, spacing and
`rotate(45)`

intact across an edit. Everything the view does not determine is **residue**, and
residue survives.

It costs strict PutPut. Take a2=get(s) and a1=get(s), with
s written non-canonically. On the left the first put reprints, the second skips, and the
non-canonical spelling is gone; on the right the single put skips and the spelling survives, giving two
different strings that encode the same drawing. What holds is PutPut *up to observation*:

and strict PutPut holds whenever a2=get(s). This is a deliberate trade rather
than a defect: canonical printing would restore the law and remove the property that makes the editor
usable on source a human wrote. Weakening a law to a quotient is the usual move when residue matters
[[Czarnecki et al. 2009]](#). `test_putput_skip_rule_corner`

in the appendix targets this case
directly, since random generators are unlikely to reach it.

Two things make SVG harder than the string examples in [[Foster et al. 2007]](#): the source is *nested*, and every address is relative to a source that editing can change.

**Notation.** Composition is written in diagrammatic order throughout: ℓ1∘ℓ2 applies ℓ1 first, so the outer lens is on the left. This is the reverse of the usual convention for functions, and matches the direction a gesture travels.

**The tower.** Four types and three lenses:

`Function.prototype.toString()`

; Doc is the bytes of the `svg`

template literal inside it; Attr is one attribute value; A is what the gesture actually manipulates: a number, a point list, an affine matrix, a list of path operations. A drag of a shape is the composite
ℓlit∘ℓattr(p,transform)∘ℓtransform:Src⇀R6
and structural editing branches off one level up, at ℓkids(p)=childrenLens(p):Doc⇀Elem∗, whose view is the list of child *source slices*, so that every child carries its own residue and only the children that changed are reprinted.

**Addressing is dependent, and that is the interesting part.** For a document s let Path(s) be the set of addresses of its elements. ℓattr is not a lens but a *family* indexed by that set, and the set depends on the source:

The two kinds of put differ exactly here:

Path(putℓattr(p,n)(a,s))Path(putℓkids(p)(k,s))=Path(s)=Path(s)in generalAn attribute edit preserves the address space; a structural edit does not. That one inequality is the reason the editor holds a selection as a *path* and re-resolves it after every put instead of caching a node, and the reason handles are refreshed after an attribute edit but cleared after a structural one.

**Partiality is part of the type.** ℓlit refuses text that would not survive re-entry into the template literal; ℓkids refuses a string that is not exactly one element. So put is partial, put:A×S⇀S, and composition intersects the domains:

The editor's contract is that this partiality is *visible*: a gesture whose put is undefined is declined before it previews, and the handle is drawn locked with the reason ([ §7 Interpolation and holes](#)). Refusing is the domain being honest, not a failure.

**Composition preserves the laws.** If ℓ1 and ℓ2 are well-behaved on their domains then so is ℓ1∘ℓ2. GetPut:

using GetPut of ℓ2 then of ℓ1; PutGet is the same argument read downwards. This is why the laws are established once per level and never re-argued for the chain. It is also why a new microsyntax lens is a self-contained obligation: prove it at its own level and the tower is unaffected.

The weakened form composes too. Each level keeps PutPut only up to observation ([ §2.2 Residue, and why PutPut bends](#)), and since (ℓ1∘ℓ2).get=ℓ2.get∘ℓ1.get, the composite's quotient is exactly the composite's own

`get`

, so "up to observation" at each level gives "up to observation" of the whole chain, with no further loss.One rule separates the maths from the pointer:

Tools emit commands, commands are lens puts, one writer applies them.

A tool is a pointer state machine (press, move, release). It may draw a preview into an overlay layer and mutate the live DOM; it may not persist anything. What it produces at the end of a gesture is a *command*: a pure function from the document text to new document text, or an attribute value to write at an address. The writer is the only code in the notebook that redefines a cell.

The layers, what lives in each, and what each is allowed to know are shown in the figure below; it is itself an `svgLens`

drawing, so the stack can be edited by the editor it describes. Nothing below L4 touches the DOM or the pointer, so L0–L3 is testable outside of a browser, and is ([ §2.1 The laws](#)). Two consequences worth stating:

**The source is the truth; the DOM is a projection.** [[Larkworthy 2026]](#) calls the general arrangement *source-last*: the live runtime is canonical and text is recovered from it on demand, and this editor applies it one level down, to the bytes inside one cell. A gesture is therefore an ordinary edit; what that buys, and why the resulting feedback loop terminates, is [ §4.1 Propagation, and why the loop closes](#).

**The browser stays the authority on geometry.** Hit testing is `elementsFromPoint`

, bounding boxes are `getBBox`

, screen-to-user conversion is `getScreenCTM`

. The lenses never re-implement SVG semantics; they only rewrite the text that produced them.

**Commands are endomorphisms of the source.** Currying a put gives

so every lens at every level of [ §3 The SVG tower: addressing and nesting](#) induces a source-to-source function, and the command layer needs no vocabulary of its own: insert, delete, reorder and the gizmo operations are all of this shape. Commands compose by ordinary function composition, and the skip rule says a null edit is the identity of that monoid:

This has an operational consequence, not just an algebraic one: the writer compares the produced source with the old one and returns before touching the runtime when they are equal, so a gesture that ends where it started causes no recompute at all.

**Disjoint addresses commute.** Dragging n selected shapes is n commands, one per address, and for p, q neither a prefix of the other

because an attribute put splices a byte range inside its own element and leaves the element list (and therefore every other address) untouched. So "one put per shape" needs no ordering discipline, and `test_commands_commute`

in the appendix checks it on random documents. Structural puts are excluded from the claim, and genuinely do not commute: they renumber the addresses the other put is holding.

**Tools are folds over pointer events.** A tool is a state machine

where δ may draw into the overlay and mutate the live DOM, and ε is consulted once, at release. The preview is a projection that is thrown away; the command is the only thing that survives the gesture. Tools sit in a priority list and the first whose hit test claims the pointer owns the gesture, so adding a tool is adding an element to a list; it cannot interfere with the levels below because nothing in Q can reach S.

**Propagation is the runtime's, not ours.** The writer applies the command and installs the result with `Variable.define`

:

Nothing in the editor notifies anything. `editor-5`

's buffer, the change history and every dependent cell update because the runtime recomputed a variable, which is what it does whenever a definition changes.

**Why the loop closes in one step.** The editor is the value of the cell it rewrites, so a gesture edits its own producer: the drawing is downstream of the gesture that changed it. That is a feedback loop, and it terminates for a reason worth naming: PutGet. The recomputed view is already the view the gesture asked for,

so the new node has nothing left to write and emits no second command. One gesture, one command, one recompute. Had the design gone the other way, mutating the DOM and deriving the source from it, the loop would need a fixed point and an argument that it converges. Here the law does that work.

The smallest useful lens: one attribute of one shape. The triangle's three corners live in a single
`points`

attribute; the rest of the source (a comment, the fill) must come through every edit untouched.

`attrTextLens(i, name)`

views the *text* of one attribute of element i; `parsePoints`

views
the numbers inside that text and puts them back into the same slots. Composing them gives a lens from
the whole document to the corner coordinates, and that composite is what a drag on a vertex writes
through.

Click the triangle, then drag a corner. The readout below reports the put that just happened: which attribute changed, and its value before and after. Two things hold, computed on each drag rather than asserted:

`points`

attribute is written, and only the corner you grabbed changes value; the fill, the
comment and everything else in the source are left exactly as typed, because put
splices the new value into the existing string instead of reprinting the whole element;Attributes are the easy half. The structural lens is harder, because inserting an element is where a naive implementation reformats the document.

childrenLens(p):Document⇄String∗The view is a **list of child source strings**, not parsed nodes. With strings, `insert`

, `delete`

and `reorder`

are `splice`

on an array, and a moved child carries its own bytes with it: attribute order, inner spacing, its own comments. With parsed nodes the commands would be tidier and every structural edit would reprint half the file.

The view does not contain the text *between* children: newlines, indentation, comments sitting in the gaps. That is residue again, and `put`

has to re-thread it: each surviving gap stays with its neighbour, and a genuinely new child gets only the *indentation* of the gap it lands in. An early version gave a new child the whole gap, so every insert duplicated the comment above the first child; four inserts produced five copies. The regression test is in the appendix, and the button below runs the same operation.

The commands are pure functions of text, with no DOM and no pointer, so the same code runs under Node in CI and under a finger here ([ §4 Tools, commands, one writer](#)). Working at the granularity of operations rather than states is the move edit lenses make

Use `[`

and `]`

to move the elements forwards and backwards in the child list

Everything so far assumed the drawing is written out literally. A drawing may also *interpolate*
values from elsewhere: `transform="translate(${shift} 0)"`

reads `shift`

from a slider. The editor
tolerates interpolation. The drawing renders, and the literal text around a hole stays editable; what
the editor will not do is write back into a hole.

The source text of an attribute is cut into **slots**. Each slot is either a literal run of digits or
an interpolation `${...}`

, and only the first kind is somewhere a gesture can write:

| slot | can a gesture write it? |
|---|---|
| literal digits | yes: spliced in place, exactly as in
§5 A first lens: one attribute of one shape |

`${...}`

A gesture that touches only literal slots commits as usual. A gesture that would have to change a hole is declined: any literal slots it also moved are written, the hole is left byte for byte, and the readout reports the mix. When the whole value is one hole there is nothing to write, so the gesture is refused outright.

Refusing is a guarantee, not effort withheld. A hole could in principle be *inverted*: resolve
`${shift}`

to the slider and set its value, or solve `${Math.sin(t)}`

numerically. Inversion by
reflection into an upstream view was tried and removed. It is fragile, it rarely holds, and its result
is indistinguishable on screen from an exact edit until it silently drifts. Declining instead keeps
every law in [ §2.1 The laws](#) true of every accepted gesture, and the handle says which case it is in.
The term

Three rectangles, two sliders. **The first** is written literally, so a drag rewrites this cell's
source and the box moves. **The second** carries `transform="translate(${shift} 0)"`

: move the slider
and it follows, because the drawing interpolates the value; but *drag* the box and the editor declines,
because the number lives in the hole and this editor does not write back into an interpolation.
**The third** carries `rotate(${spin} 32 100)`

and behaves the same way for rotation.

Interpolation is tolerated, not inverted: the slider feeds the drawing, and a gesture that lands on a hole is refused rather than guessed. Watch the table underneath: every gesture reports whether it wrote literal source or hit a locked hole.

The lenses so far answer one question: given a new view, what source produces it. A tool asks something narrower and carries a memory with it: given a *pointer delta* and the context a gesture accumulates, what edit does the source get? That memory is what a state-based lens lacks, and naming it as one value is what lets a single law describe every tool's edit instead of one law per tool.

The generalisation is already in the literature the paper cites. Where a *state-based* lens maps states to states, an **edit lens** [[Hofmann et al. 2012]](#) maps *edits* to edits through a **complement** (a memory the translation is allowed to keep), and [[Johnson & Rosebrugh 2016]](#) axiomatises it alongside the asymmetric **delta lens** of [[Diskin et al. 2011]](#). The two frameworks do two jobs here. The delta lens is the shape of the *writer*: the source is authoritative, the drawing is derived. The edit lens is the shape of a *tool*, because what a tool has that a state-based lens lacks is exactly a complement. The dictionary is one line. **The complement is that memory:**

| edit lens | svg-lens |
|---|---|
| X, MX | the cell source; the edits a command can make to it |
| Y, MY | the rendered drawing; pointer deltas |
| C | `ctx.state.drag` : the origin, the grabbed targets, the hit list, the snap boxes |
| q:MY×C⇀MX×C | the tool |
| K | "the DOM on screen is what this source renders to" |

Making the delta a value is the whole of the framework. A tool returns a `gestureDelta`

(one of `attr`

, `command`

, `select`

, `view`

, `clip`

), and three functions consume it: `previewDelta`

paints the live DOM, `commitDelta`

writes the source once, `revertDelta`

throws the preview away. One write path, so every law in [ §8.1 The laws a tool owes](#) is stated about the delta and checked in one place rather than re-proved per tool. It is also what lets the registry stay

`svgLens(node)`

uses the default tools, `svgLens(node, { tools: [toolVertex] })`

pins a vertex-only editor, and `svgLens(node, { tools: (d) => [myTool, ...d] })`

extends without restating the defaults: a test runs one tool in isolation and a figure shows a reduced editor by the same mechanism.Five laws come straight from the axioms [[Johnson & Rosebrugh 2016]](#). The null gesture is the sharpest.

| law | what it says here | what it rules out | |
|---|---|---|---|
T1 |
p(1,c)=(1,c) | a null gesture writes nothing and returns its memory untouched |
a click that grabs nothing creating a shape, or a gesture losing the context it holds |
T2 |
p(mm′,c)=(nn′,c′′) | translating a composite gesture equals composing the translations | float drift — two gestures ending at the same point committing different bytes |
T3 |
PutGet | the gesture you made is the gesture the committed source shows | a preview that looks right on screen but not once the source is reloaded |
T4 |
PutInc | a gesture commits against the state it began from |
a commit that mints a new node letting a follow-up write land on a stale document |
T5 |
consistency | if the DOM agreed with the source before, it agrees after | makes T3's screen-level check a consequence, not a separate assertion |

**Partiality is the formal content of "a tool must decline cleanly."** A gesture outside a lens's domain is undefined, so its translation is undefined too: the action is partial. `onPointerDown`

returning `false`

is that partiality — a tool with nothing to say about a press declines it rather than falling through to drop a shape on an empty click, which is why an empty-canvas tool gates on `ctx.pick`

first. The same partiality runs through every surface that reaches the DOM: a command declines by returning `null`

from its plan, and an affordance chip greys out exactly when its command declines. One notion of "not applicable here" spans pointer, command and chrome.

Six more laws are ours, not the papers':

`false`

has changed nothing, and installing it cannot change what earlier tools do. Registry order is priority, so tool sets form a non-commutative monoid under concatenation. This is the law that makes a tool a `rebase(path, cmd)`

equals re-locating the same element after the command runs: operational transformation's TP1, one-sided.`ctx.pick`

, so they cannot drift apart. It also pins the group policy: a click takes the outermost unopened container, a double-click descends a level, Escape ascends.`ctx`

and `getScreenCTM`

already carries the viewBox, with alignment snapping off. (Snapping is magnetism measured in Three things that looked like special cases are textbook constructions with textbook laws: the **shape registry is a family of prisms** (tag dispatch is a sum type; preview∘review=Just is what each `rect`

/`circle`

/`path`

entry owes), **hit-testing is an affine** (a focus that may fail, whose law is that a failed get makes put a no-op, T1 again), and **multi-selection is a traversal** (so "set the fill on the selection" is traversal ∘ lens, and align, distribute and group-edit come with it). All of them run live: `test_gesture_*`

and `gestureLaws`

check the laws above over a random corpus of gestures, on the same `forAll`

harness the lens laws in [ §2.1 The laws](#) use.

**Bidirectional programming.** The lens formulation, the laws and the well-behaved / very
well-behaved distinction are Foster et al.'s [[Foster et al. 2007]](#), addressing the view-update
problem posed first for databases [[Bancilhon & Spyratos 1981]](#). [[Czarnecki et al. 2009]](#) survey how many
fields arrived at the same structure independently. What differs here is the source:
[[Foster et al. 2007]](#) lens trees and strings, this notebook lenses the executing function's own
text, so the residue that must survive is the author's formatting and `put`

splices into bytes that
are also a running program.

**Sketch-n-Sketch.** The closest system [[Chugh et al. 2016]](#) [[Hempel & Chugh 2016]](#)
[[Hempel et al. 2019]](#): an SVG editor where you write a program, manipulate the output, and the system
updates the program. It is considerably more expressive, handling abstraction, loops and recursion via
trace-based provenance and later output-directed synthesis [[Hempel et al. 2019]](#), and it can produce a
program the user never typed. Three differences, none a claim of superiority:

**Lenses in practice.** Cambria applies lenses to schema evolution in local-first documents
[[Litt et al. 2020]](#); edit lenses [[Hofmann et al. 2012]](#) shift the currency from
states to edits, which is the same instinct as our command layer, though we implement it concretely
as splices on child source strings ([ §6 The structural lens](#)).

**The host this depends on.** The editor relies on the system it runs in being *source-last*
[[Larkworthy 2026]](#): Lopecode keeps no canonical saved file, treats the live runtime as the
source of truth, and recovers each cell's text on demand from `Function.prototype.toString()`

. There
is no file on a server that the definition is a copy of, and no save step for an edit to fall out of.
Read the other way, this notebook is a test of that paper's claim that a plurality of editing surfaces
follows from runtime primacy: `editor-5`

edits a cell as text, `editable-md`

as prose, `sticky`

edits a literal by manipulation, and `svgLens`

edits one as a drawing. None is privileged, and since
all four go through `Variable.define`

([ §4 Tools, commands, one writer](#)), each one's edits are visible to the
others.

**Notebooks and liveness.** Liveness has a standard scale [[Tanimoto 1990]](#), but it measures
feedback from program to output; the quality at issue here is the reverse. [[Horowitz & Heer 2023]](#)
name persistence as what separates a rich widget from a programming system: interactions with a
rendered tool "cannot be 'saved' back to the notebook". This is one answer for one domain: the
widget's output *is* the cell, so there is nothing to save back. [[Edwards et al. 2019]](#) ask for costs
to be reported alongside benefits, which is what [ §10 Future work](#) is for.

**The literal lens is not specific to SVG.** `literalLens`

finds a tagged template in a cell's own
definition and lenses its text; everything above it is SVG-specific, everything below is not. The same
primitive points at markdown in an `md`

cell, CSS in a `css`

cell, or JSON in a data cell: a
click-to-edit prose editor and a colour picker that rewrites a stylesheet differ only in the
microsyntax layer. This is the generalization worth doing next.

**Writing through holes.** Today a hole is tolerated and locked ([ §7 Interpolation and holes](#)). Two ways to make one
writable are visible from here, both deliberately out of scope: resolving a hole to another cell's
literal, which makes the lens target the dataflow graph rather than one cell, and numerical inversion
for holes feeding arithmetic, the approximate solve

`@tomlarkworthy/manipulate`

already implements.
Reflection into an upstream view was tried and removed for being fragile; either replacement would have
to keep reporting that its guarantee is weaker than an exact edit's.**Structure the domain refuses.** An interpolation in element position is out of scope, because
document-order indices would stop matching the DOM. Addressing children by identity rather than
position would lift the restriction, at the cost of putting ids into a drawing the author is writing.

**What it costs.** Reported in the manner [[Edwards et al. 2019]](#) argue for: the editor reaches only
drawings written literally; the parser is a deliberate subset of XML (no CDATA, no entity decoding, no
nested `<svg>`

); PutPut holds only up to observation ([ §2.2 Residue, and why PutPut bends](#)); undo declines rather than
merging when another writer has touched the cell; each gesture commits once, so dragging a 200-element
selection is 200 puts; and since a commit recomputes the cell (

The tests come first because they are the specification: every law quoted above is a cell below, run on each load with a seeded generator. After them, the implementation in dependency order: lens core, SVG microsyntax, source lenses, commands, then the editor.

Deterministic replacement of fast-check: a seeded PRNG (`mulberry32`

), domain generators (`arb`

), and `forAll`

which throws the counterexample on failure. Fixed seeds keep every test cell rerunnable with identical results. `arb.anyFinite`

draws random 64-bit patterns, so the exact round-trip claims are exercised across the full double range, subnormals and -0 included.

Each `test_*`

cell property-checks one lens (or one exactness claim) with a fixed seed.
The dashboard near the top of the notebook aggregates them.

All printing uses `String(n)`

, all parsing uses `Number(s)`

. Lenses are total on their stated
domains and throw outside them (parseable strings, nodes carrying the focused attribute/child,
matrices with det ≠ 0, path data with separated numbers, with no `10-5`

abutment or compressed arc flags).

Affine matrices as a flat `[a,b,c,d,e,f]`

, their product, and the screen-to-user map (`getScreenCTM`

) that every measurement is taken through.

The `transform`

attribute as an editable list of operations (translate, rotate, scale) plus the gizmo that rotates or scales about a point held fixed.

The determinant, the matrix inverse and its involution law (a screen delta sent back through an element's own frame), and the small accessors that read an attribute or child out of a parsed node.

The `d`

attribute parsed into absolute commands and printed back: the lens the pen and vertex tools write a path through.

Everything above focuses *inside* an attribute. These focus *outward*, until the source is the
JavaScript of the cell itself:

| Lens | Source | View |
|---|---|---|
`literalLens(alias)` |
a cell definition's source text | the template literal inside its `alias(…)` call |
`attrTextLens(i, name, dflt)` |
SVG document text | element `i` 's attribute string (`dflt` when absent) |
`cellAttrLens(alias, i, name, dflt)` |
cell source | that attribute, addressed from the cell |

`literalLens`

locates the literal by parsing the definition with acorn and taking byte offsets, the
same technique `@tomlarkworthy/sticky`

uses for its persistence slot. Its domain is definitions
containing one `alias(…)`

call whose argument is a template literal with no `${}`

interpolations,
and views that contain no backtick, backslash or `${`

(they would not survive re-parsing).

Composing outward from a typed view gives a lens from **cell source** straight to **matrix**:

```
compose(cellAttrLens("svgLens", 4, "transform", "matrix(1 0 0 1 0 0)"), transformLens)
```

That composite is what a drag commits, and `test_cellSourceLens_laws`

checks its laws.

A forgiving scanner over the SVG text, the tree it builds, and `nodeAt`

/ `pathOfIndex`

for addressing an element by a path that survives structural edits.

Reading and splicing a single attribute, the `style=""`

and unit-bearing-number lenses, `setProperty`

(which writes where the property already lives), and `refsOf`

for the `url(#id)`

a shape points at.

Paint and stroke properties as *data*, so the panel and the on-canvas chips read one list and adding a property is a line here, not a branch in a form.

The list of an element's children as a lawful view, and the pure document-to-document commands built on it: insert, delete, reorder, group, ungroup, the copy/paste codec, and minting into `defs`

.

Segments, subdivision and anchor removal for polygons and paths, and the address rebasing that keeps a vertex selected across an edit that changes the command list.

`svgLens(node)`

returns the node it was given, with pointer handling attached. During a gesture only the live DOM changes: no source is written, so dragging is as cheap as `setAttribute`

. On release the gesture commits once, through `compose(cellAttrLens(…), transformLens)`

, and the DOM then adopts the source's exact bytes: if you drag a shape back to where it started, the readable `translate(228 128) rotate(-4)`

is still there, because GetPut says so.

The new definition is built with `realize`

(which routes through `importShim`

on lopecode) and installed with `Variable.define`

, the same call `editor-5`

makes when you type in a cell.

Gestures call `define`

, the same kind of event as typing: the cell is invalidated, recomputes, and produces a new node.

That has a cost (the node the cell handed out is replaced on every commit) so state that must outlive a gesture cannot live in the node's closure. Undo history, the selection and the active tool are held in `lensState`

, keyed by the `Variable`

, which `define`

mutates in place. The selection is restored by *path* rather than index, which is why it is addressed that way.

Rendering evaluates the template rather than parsing its text, so an interpolated `svg`<circle r="${r}"/>``

renders the same way a literal one does; what a handle over a hole may write to is a separate question ([ §7 Interpolation and holes](#)).

`applySource`

re-reads `_definition`

after the `await`

and abandons the put if it changed underneath, since `editor-5`

may have rewritten the cell mid-gesture.

`svgLens`

is wiring and nothing else. The parts it wires:

| Cell | Owns |
|---|---|
`svgTarget` |
which variable this node is, the parameter name the cell calls `svgLens` by, and the document text |
`svgWriter` |
`applySource` / `commit` / `runCommand` : the only code that assigns `_definition` |
`svgOverlay` |
the handle layer, and the `isOwn` predicate that keeps the renderer off it |
`svgFocus` |
which element is selected and where its handles are drawn |
`svgTools` |
the tool registry: move, vertex, transform, marquee, the shape and pen tools, and the affordance dispatcher |

A tool is `{id, onPointerDown, onPointerMove, onPointerUp, onDblClick}`

. `onPointerDown`

returns true to claim the gesture; registry order is priority. Tools read the document, preview in the live DOM, and hand a command or a commit to the writer; they never write the source themselves, and none of them knows what a lens is. Adding a tool is adding a cell:

```
svgTools.push({ id: "rect", onPointerDown(ctx, e) { … } });
viewof svgTools.dispatchEvent(new Event("input"));
```

Or take the set at the callsite, which pins one drawing to exactly the tools you name: how a test runs a tool in isolation, and how a figure shows a reduced editor. The selection chrome (the affordance chips) is the same kind of registry, so `affordances`

strips it the same way:

```
svgLens(node, { tools: [toolVertex] }) // a vertex-only editor
svgLens(node, { tools: (d) => [myTool, ...d] }) // extend without restating the defaults
svgLens(node, { tools: [toolMove], affordances: [] }) // a bare drawing: handles, no chips
```

The figures above use exactly this: each loads only the tool its point needs and no chips, so the chrome does not distract from the one gesture being shown.

The writer stays ignorant of selection: it reports what it did on the `lens-put`

event, and the handles follow, refreshed after an attribute edit and cleared after a structural one, because a structural edit shifts every address after it.

The draggable points a selected polygon or path exposes: where the vertex tool grabs.

Per-tag geometry as data (what each shape reads, writes and offers as handles) so a new shape is an entry here rather than a branch in every tool.

The four pieces `svgLens`

wires: which variable this node is, the only code that assigns `_definition`

, the handle layer, and which element is selected.

`gestureDelta`

values and the three functions that consume them (`previewDelta`

paints, `commitDelta`

writes once, `revertDelta`

discards): the one write path every tool goes through.

Each tool claims a gesture through `onPointerDown`

, previews in the live DOM, and hands a delta to the writer: move, vertex, transform, marquee, scope, the shape and pen tools, and scribble.

A headless fixture that plays a recorded gesture and checks T1–T11 over a random corpus, plus the per-frame budget that catches a dropped selection or a scale flash.

Keyboard/menu commands whose `plan`

returns a delta or declines, and the chips that surface those same commands on a selection, both writing through `commitDelta`

.

`svgLens`

itself: the state that outlives a remount, and the wiring that turns all of the above into an editor attached to one node.
