# Ollin design notes

The engineering design intent behind the planned work in [`ROADMAP.md`](roadmap.md). The roadmap is the short version (what's planned and how to help); this is the long version (how each piece would be built, the specific APIs involved, and why). A couple of shipped items stay only where their design rationale still guides future maintenance or evolution (extending the examples set, the Swift-idiom track); anything fully done and self-contained lives in its own doc or `CLAUDE.md`, not here.

These notes assume Ollin's two ground rules: it targets Apple platforms only (no cross-platform abstractions), and the bare p5-style API is sugar over a public, typed core, so a feature is built on the core first and given the bare call second. Geometry-emitting calls take a `draw` verb prefix (`drawCircle`, `drawRect`, and so on).

## Swift Playgrounds and iOS

The design home for the leg that runs a sketch on the phone and the tablet. The canvas is one view on either window system and `Apps/OllinSketchApp` is the reference project for wrapping a sketch as an iOS app; these notes carry what's ahead: the Swift Playgrounds on-ramp (the closest Swift gets to the p5.js "open the editor and type, see it move" onboarding, on-brand for the "learn it in an afternoon" goal), the tethered edit loop, and the smaller device affordances. The roadmap section is [The sketch on a phone, and Swift Playgrounds](roadmap.md#the-sketch-on-a-phone-and-swift-playgrounds).

- **Playgrounds questions, in dependency order.** An App Project (`.swiftpm`) is a Swift package whose manifest declares an `iOSApplication` product through `AppleProductTypes`, built by the Playgrounds app's own bundled toolchain, not the Mac's installed one. So: (1) the toolchain is the gate, and it lags. The SDK half is met, and the Swift half is not: the shipping app bundles Swift 6.2 (its `PlaygroundCompiler.framework` carries the version string, and its own modules are built for `ios-macabi`, which is the Catalyst preview), while the root manifest refuses anything below Swift 6.3. That floor is earned rather than cautious, since a 6.2 build fails deep in the framework instead of at the manifest, so nothing here can be scaffolded until an app update carries 6.3; re-read that version string after each one. (2) The dependency arrives by git URL, which the public repository serves, and that is also the only shape a tablet can use. A local `path:` dependency is less of a desk bench than it looks: the Mac app is sandboxed with free access to `/Applications/` and `~/Documents/Playgrounds/` alone, so a checkout anywhere else reads to the user as having no access to the package, and pointing at one means keeping a copy of the tree under that Documents folder. (3) It runs on real GPUs, the Mac's or the tablet's, so the renderer's device tier applies and the simulator wall is irrelevant here; don't chase the simulator to test Playgrounds. The Mac app previews an App Project through Mac Catalyst, so a platform seam there keys on the operating system rather than on whether AppKit imports, and a runtime shader compile names its language version. Prefer the Swift Playgrounds app's App Projects over the semi-deprecated Xcode Playgrounds throughout.
- **The starter is a generator kind, not a hand-kept sample.** `ollin new` writes packages, and an App Project is one, so the Playgrounds starter is a `ProjectKind` beside the others rather than a checked-in sample that rots: the manifest with the `iOSApplication` product, a host file in the reference app's shape (a `SketchView` in a `WindowGroup`, `SwiftUI.Scene` spelled out since the framework has a `Scene` of its own, safe areas ignored), and one small touch-driven sketch, seedable from the examples the way `--from` already seeds. Unlike the `ios-app` kind, it needs no Xcode project and no signing team.
- **Keep shaders Playgrounds-safe.** Swift Playgrounds' support for SwiftPM build-tool plugins is unreliable, so don't go plugin-only for shaders. Runtime source compilation stays a first-class loader, and it is also what makes a shader edit instant on a device.
- **Scope inside the sandbox.** Drawing, touch, the parameters, and the shader compile are the Playgrounds surface. The export tier writes files where the sandbox has opinions, so it stays desk work until someone asks; nothing about drawing needs it.
- **Beyond the cable: the sketch on a phone that is not here.** The installed loop needs pairing, which reaches a phone on the cable or the same network and never one across the internet, since an install goes through `devicectl`. The picture stream is the way to a phone anywhere, and what it still needs is a transport past the usbmux tunnel. The sender is handed a connected socket and the phone's listener already accepts on every interface, so the local network is the short step: find the phone by Bonjour and dial it directly. Across the internet the phone has no address a Mac can dial, so it needs a rendezvous both ends reach out to, which Ollin does not run. Any network path also needs pacing the cable never asked for: a bit rate that follows the measured throughput, and a way for the phone to ask for a keyframe after a loss.
- **The tablet keyboard.** UIKit press events (`pressesBegan`/`pressesEnded`) feed the same key path the desk view feeds, so `keyIsPressed` and the key callbacks work with a hardware keyboard on a tablet.
- **Verification:** iOS builds verify on a Mac with `xcodebuild -destination 'generic/platform=iOS'`; anything hands-on needs the device.

## Color additions deliberately kept out

Rationale that still gates future color work:

- **Other color spaces stay out.** Classic Lab/LCH (the 1976 space OKLab was built to fix; interpolating through it bends blues toward purple), CIECAM16/JCh (a full appearance model that needs viewing-condition parameters; OKLab was fitted to give most of that benefit without the machinery), and DIN99/DLCH (an industrial color-difference standard). Any of them can arrive later as a plain conversion if matching an external spec calls for it.
- **Further curated palette collections stay license-gated.** Collections scraped from ColourLovers and similar sites are not license-clean and stay out; anything added must carry a permissive license the way the ColorBrewer data does.

## Layered effects and compositing

OPENRNDR-style effects that compose in layers: draw into off-screen layers, run filters over them, composite with blend modes. The substrate (`renderTarget` / `withTarget` / `filtered` / `combined` / `postProcess` / `generate` / `feedback` and the `compose { }` DSL) is inventoried in `CAPABILITIES.md`; what belongs here is growing the catalog on top of it. The reference menus guide what else to add and how to shape it: AsyncGraphics (its `Graphic` is an `MTLTexture`, effects are functions returning new graphics, plus a blend/stack model; borrow the compositing architecture, not its async call shape), SwiftUIShaders (MIT, a menu of forty-odd post-process effects such as holographic, kaleidoscope, datamosh, and thermal, to mine and reimplement), and ofxFX (MIT, Patricio Gonzalez Vivo: composers, the bokeh/ripple/flow passes still worth taking). More two-input ops (layer-blend variants, chroma keying, wipes and other transitions) slot in as further `Combine` cases. Same stance throughout: study and reimplement, credited in `ATTRIBUTION.md`.

- **A measured field that is exact near the edge.** Within a pixel of an edge, `.distanceField` is the distance to the nearest of a row of discrete seeds, one per crossing found between two pixels, so a pixel lying between two seeds along the edge measures up to about 0.3 pixel long. The layer styles show it (measured 2026-10-08 on a 360-gon's multisampled edge): a one-sided outline under two pixels wide carries up to 0.06 pixel less than its width and a centered one up to 0.125, and the bevel takes its direction from a Sobel stencil two pixels wide because the direction channel turns by up to atan(0.5/d) between neighbors. The canonical answer is the anti-aliased Euclidean distance transform of Stefan Gustavson and Robin Strand (Pattern Recognition Letters, 2011): an edge pixel's distance to the edge estimated from its own coverage and the local gradient, with the offset to the edge carried by the flood. Measure it against the current seeds first, on the zero contour, `cuttingTheFieldGrowsTheShapeByThatMuch`, the outline widths, and the flat light that shares the field, since it moves every consumer of the field.

## 3D mode

2D stays the default and most sketches stay 2D, but 3D is a mode in its own right: a sketch opts in by setting a camera and, on the same Metal core, draws depth-tested point clouds and lit solid primitives, with richer shadows and materials as the mode grows. The iPhone point cloud renders through it, and visionOS and AR build on it. openFrameworks and OPENRNDR set the capability bar here, so the target is known. (The foundation, from the camera, depth buffer, and transform stack through point clouds, meshes, lights, materials, and cast shadows, is inventoried in `CAPABILITIES.md`; the notes below build on it.)

- **The further scene-import legs.** Over the structure-preserving import and animation playback a loaded `Scene` gives (see `Docs/3D/Scenes.md`): vendor `EXT_`/app-specific glTF extensions beyond the ratified `KHR_` set.
- **Material-from-file refinements.** A merged `loadMesh` model uses its first base-color material (glTF/USD/OBJ-MTL). The UV-origin convention for a Model I/O `imageFromTexture` result is unverified and may need a v-flip. A material carries **one** wrap mode for its whole map set, read from the file's first map; a format that gives each map its own sampler (glTF does) would need a wrap per slot to reproduce exactly, which no exporter seen so far asks for.
- **The light grid's next two steps, if the cost ever asks for them.** The per-tile cull spans the whole depth range, because there is no depth pre-pass to bound a tile by: a **depth-bounds** refinement (min/max the tile's own depth, then cull against that slab, which is the difference between a tile and a cluster) tightens it for a scene with lamps at many depths. And the inline path below `OLLIN_MAX_LIGHTS` culls nothing at all, which is why a measured 16-lamp culled frame beats an 8-lamp inline one; **culling every lit frame** would pick that up, at the cost of the byte-identity the whole 3D reference set rests on, so it is a 1.0-shaped call rather than a free win.
- **Build order.** Softer shadows (and supporting more than one caster at once) are the rendering-heavy step. The tier-one material recipes are pure data over the existing light/material model with no renderer change, so they land cheaply (the way the lighting presets did): a `Material` surface beside `specular`/`specularSharpness`. Each step is usable on its own.
- **visionOS is a different render loop.** Immersive rendering doesn't use `MTKView`/`SketchRunner.draw(in:)`; it uses CompositorServices (`LayerRenderer`) driven by a manual render thread, plus ARKit world tracking. Keep the per-frame loop behind a seam that either an `MTKViewDelegate` or a visionOS layer renderer can drive, rather than assuming `MTKView` everywhere.
- **Reference.** swifty-creatives is the concrete study: it ships both a normal `MTKView` renderer and a visionOS `RendererBase` with a manual `renderLoop()` over `LayerRenderer`, and it's 3D-first (camera, depth, box and 3D text). Borrow the structure (camera plus depth pipeline plus dual render loop) and write our own.
- **Ray-traced reflection refinements.** The **subsurface diffusion** behind `Material.scattering` does not reach a reflection: its blur runs over the primary view's own buffer, and the transmittance half needs a caster's depth the traced direct light never reads, so a skin or marble body arrives in a mirror with its shading unsoftened. That is bigger than the compact per-geometry finish record the other finishes are stored in, which is what makes it the one left.
- **A hologram material.** Stam's diffraction shader (*Diffraction Shaders*, SIGGRAPH 1999) gives a grating's color from the half-vector's component along the grating direction and the groove spacing; summed over the visible wavelengths through the spectral taps the thin film already uses, the lobe lands as a gated branch of the surface-mapped fragment beside the thin film, so a material without it stays byte-identical. The grating direction can turn per facet of a normal map, which is what makes a crumpled sheet holographic rather than one smooth rainbow. Its traced form is one more entry for the sheens list under *Rendering and color frontier*.
- **Richer 3D physics: the solver's own remaining gaps.** The one thing still ahead is **soft-body reach**, and it is upstream's rather than Ollin's: self-collision and cloth-against-cloth are gaps in the solver, so a folded sheet passes through its own layers, and tearing needs a shared vertex split and a rebuilt constraint set mid-simulation, which it cannot do at all. Both stay out until the library grows them. The solver's **volume constraints** (tetrahedra held at their rest volume) stay out for a different reason, a measured one rather than a missing one: built and probed against the shipped `pressure`, they tie on holding a weight, tie on coming back from a dent, and lose on coming back from a hard squash, because their residual is formed from an *unsigned* volume and so reads an inverted tetrahedron as correct, where a gas law's push grows without bound as the space shrinks. The numbers and the two things that would reopen it (a vertex-follows-tetrahedron binding, or a signed volume constraint) are in [`ARCHITECTURE.md`](architecture.md). The solver's neighboring **hair module** stays out for four measured reasons. It accepts no force, so the only way to move a strand is to move the head, where Ollin's cloth and rope examples are driven by `applyForce`. It collides only with `ConvexHullShape`, and Ollin's figures are meshes and capsules. It requires an authored groom and a skinned scalp, where a sketch generates its own geometry. And the rod family already exposed carries 20,000 simulated strands in a 60 fps frame against its 2,500, so the Metal backend it needs (49 shaders cross-compiled and packaged, Objective-C++ admitted into the C bridge's target, a new draw path for a foreign compute buffer) would buy a strand count `Rope3D` already reaches. The measurements and the reopening bar are in [`ARCHITECTURE.md`](architecture.md); if hair is wanted before then, build it as a **multi-strand `Rope3D`** (one soft body holding many rod chains) rather than from that module. When they arrive, the shipped rules still apply: land it as *more capability on the existing surface* (a parameter on `addSoftBody`, a call on `SoftBody3D`), never a parallel system; keep the C bridge flat-POD `extern "C"` with `JPH` never reaching Swift; keep the no-pixel-snapshot policy (determinism is per-binary, so behavioral tests pin each slice) and `unitsPerMeter` as the one scale bridge; and ship an example, tests, a `Docs/Simulation/Physics3D.md` section, and Guide Ch 31 growth together.
- **A 3D body past the world's limit.** `World3D.addBody` past `maxBodies` returns a `Body3D` the solver never made, which sits at the origin for good, with nothing said. That is the silent shape the failure rule refuses: the call can see the failure, so it throws, ruled 2026-10-15. `addBody` and its siblings that create a body throw an error naming the limit past `maxBodies`, a breaking signature change across every 3D sketch (each call gains `try`) named in the changelog as one sweep; until then the reference names the limit beside `World3D`.
- **Lines in 3D, the parts a 2D stroke has and a 3D one does not yet.** A line with 3D points draws whole while a dash, a brush, or a width profile is on, with a note. A dash wants cutting along the projected path, as the 2D stroke cuts it, with each cut point carried back to the 3D point it stands for: a fraction of the way across a projected segment is not the same fraction of the 3D segment, so the cut is mapped through the two ends' clip w (the perspective-correct parameter), and the cut point takes its depth from there. A profile multiplies the per-point half-widths the expander already takes, so it is the smaller of the two. Three outlets leave the line out today and could carry it. A recorded `Batch` has no camera, so its lines would expand at replay against the camera then active, the way a recorded point cloud draws through whatever camera `drawBatch` meets. A spatial export could write USD `BasisCurves` with a width per point, which is a line rather than a surface. And a web page could replay the line's world points and offsets through a WebGL vertex stage that does what `ollin_line_vertex` does, with the frame's camera recorded beside them; a page whose 3D is only lines would then cross without falling back to video.
- **Feature edges on a mesh rebuilt every frame.** `featureEdges()` keeps a mesh's edges while the mesh goes on being drawn, so a mesh built anew each frame is searched each frame: about half a microsecond a vertex, a 1.2-million-vertex trail mesh at 0.6 s a frame with its edges against 30 ms without (reported 2026-10-14). Its index buffer usually keeps its shape from frame to frame while only positions and normals move, so keeping the welded adjacency while the indices hold and redoing only the crease test would take most of that away; a GPU pass over the triangles is the other route.
- **Feature edges on a mesh field, and in call order.** A `MeshField`'s copies draw their faces without their feature edges: the field's GPU cull writes the arguments of the solid draws, and the edges would need the same visible-copy list to write theirs, one more indirect draw per entry. A field that wants edges today draws its copies through `drawMesh(_:instances:)`. Separately, meshes drawn one call at a time put each mesh's edges straight after its faces, so where two solids nearly touch, within the depth pull, the edge of the one drawn first can show through the face of the next; a run of mesh draws could hold its edge batches until the run ends, as an instanced call does by construction. Both wait for a sketch that shows them.
- **Lofting: what is ahead.** A loft matches its sections' contours in order and leaves out a contour some section lacks; a hole that closes on the way (a tube whose bore pinches shut) would want the partnerless contour to shrink to its own center, the way a 2D morph handles it, with the cap at that end filled as if the hole were not there. The blend across unevenly spaced stations reads the sections as evenly spaced (a uniform cubic, the one `Curve3D(curveThrough:)` uses), so the surface's speed along the path changes at a station while its direction does not; a cubic whose knots are the stations themselves would smooth that too. On a closed path the last section joins the first as it was drawn, so a loop whose sections turn progressively twists at the join rather than spreading the turn round, which the path's own frames do for their torsion. Each is a small addition to `Mesh.loft` when a sketch asks for it.
- **Volumes: what is ahead.** The march (`ShaderVolume.metal`) composites after a pass's geometry and reads its resolved depth; the pieces below extend it without changing that shape. **A grid a GPU kernel writes:** `ComputeTexture` is 2D, so the first piece is a 3D compute texture, a 3D dispatch in `compute(_:writing:)` (`RecordedDispatch` carries only a width and height today), and `drawVolume` taking it in place of a `Volume`. The light-transmittance grids already come from a kernel, so a GPU-resident grid needs no CPU step anywhere. **A volume's own shadow on solids:** a deep shadow map (or the transmittance grid read from the lit mesh fragments) so a cloud darkens the ground under it; today the medium casts nothing. **Light the medium bounces to itself:** single scattering leaves a thick white cloud at about half a white surface's brightness and its far side dark. The production approximations (Wrenninge's multiple-scattering octaves, a diffusion term on the transmittance grid) are the places to start. **The rest of the scene's light:** point (cube) and traced casters do not shadow the medium, the area kinds scatter nothing, and fog does not veil a volume. Each is a read the march does not make yet. **Two volumes that pass through each other** order as wholes by their boxes' centers; marching them jointly over their sorted intervals would interleave them. **A glow that follows a ramp:** the classic transfer function, a `Ramp` from the grid's value to the glow's color (fire from a temperature field), would be a 1D lookup beside the grid.
- **Motion for copies placed on the GPU.** A `drawMesh(_:instances:)` list inside `withMotion` keeps last frame's placements on the CPU and the velocity pass reads them per copy. Copies placed by a compute buffer, and a `MeshField`'s, never pass through the CPU, so their last frame would have to be kept on the GPU: for a compute buffer, a copy of the buffer taken before the kernel writes the frame's placements (and kept per buffer, since a sketch may write one twice); for a field, its placements already live on the GPU and stay put unless the field is rebuilt, so a moving field moves through its draw-time matrix, which a field mover would carry as one transform, and the cull's compacted list would map each visible copy back to its id. Separately, at an overdriven shutter (4) on the default tier the streaks read grainy across a fast copy's body: the reconstruction gather's taps are tier-bound (9, 15, 27), and a shutter past 1 stretches the same taps over a longer streak (to the `k` clamp), so `.detail` is the smooth one. Whether an overdrive should raise the count with the streak is the question there.
- **A copy turned by a `Rotation3D`.** `MeshInstance.matrix` composes `T · Rx · Ry · Rz · S` from a `Vector3` of angles, so a copy turned about an arbitrary axis needs the quaternion broken into x-then-y-then-z angles first (`b = asin(R02)`, `a = atan2(-R12, R22)`, `c = atan2(-R01, R00)` for `R = Rx · Ry · Rz`). That round trip is exact to about 1e-16 away from the gimbal, and loses precision as the y angle nears a quarter turn. The fix keeps the turn as the rotation it is: the placement holds a `Rotation3D` and its matrix uses the rotation's own 3x3, with the Euler form as one way to build it. The shape is ruled (2026-10-14): `rotation` becomes a `Rotation3D`, one property holding the turn, and three angles become one way to build it. The change breaks every call site that reads or writes the `Vector3`, so it lands with its rename in the changelog, and consistency wins over the two-ways-to-say-one-thing alternative before 1.0. Measure first that a field of copies built either way draws the same pixels as `rotate(_:)` plus `drawMesh` per copy.
- **The colonnade's fog.** The `3D/Effects/Atmosphere` example's fog mode (its default; aerial perspective is held under space) reads as a solid layer: below the horizon every pixel is the fog's color and only the column tops above it show, where its poster of 2026-09-26 showed the near columns clear through a mist pooled low. The frame renders pixel-identically at `4c513b60` and after the sky's sun, so the change came between 2026-09-26 and that commit; a bisect over those commits (a release build of the one example each, frame 480 with `--photo --seed 1`) would name it. The example's media shows the current picture meanwhile.
- **Depth of field on the canvas: the parts ahead.** `depthOfField()` runs over the main canvas: a `withTarget` layer and the accumulation surface (`noClear()`) stay sharp, and the pass would need the resolved depth each one keeps. The cost is a full-resolution gather (about 14 ms at 1080 by 1080 on an M2 at `.default` over a frame full of blur); the far field, whose blur is wide and smooth, could gather at half size and be brought back up, as the flare's ghosts are. **The hidden layer reaches the far field only under a veil.** The scene drawn a second time behind the first surface is read under a pixel in front of the focus, where the cover share decides how much of it shows, and under a near veil tap for a pixel behind the focus. A mesh built or loaded by hand carries no word on whether it is closed, so its back face stays out of the layer; the print check's boundary-edge count could answer it once per mesh if a case asks. Just inside the top rim of a near ball against the backdrop the canvas reads darker than the lens (0.02 over a 5 px ring on the lens figure's scene, the cover there within a hundredth): the lens looks over the ball's top from the lower half of the aperture and sees the sphere's own limb, points that face away from the eye's center and so are culled from the hidden layer as back faces; a peel that kept a closed mesh's back faces where they face some point of the aperture would carry them. A one-pixel hole in a near surface (a gap between floor boards) is found by some taps and missed by others, which shows as a faint dotted line where the lens draws a smooth one; the tap count barely moves it (the gap lines' high-frequency error against the lens read 0.0012 at the default tier and 0.0010 at the detail tier on the lens figure's scene, at 288 and 768 taps), so the lever is a read of the color at the tap's own footprint, which has to stay on one surface or it invents a blur a sharp line never had.
- **A traced sequence, cheaper.** What a path-traced sequence could still save beyond the per-pixel stop (`--pt-noise`), the frames carried across it (`--pt-reuse`), and the time budget a frame (`--pt-seconds`), motivated by a sequence of two thousand polished-metal copies under a lighting-only environment at 1080 by 1920, where 128 denoised samples take 12 to 14 s a frame and 256 take 27, so a minute of footage is a night or a day. *A sharper history read:* the carried frame is read through the four texels around where a pixel was, and every frame a surface moves by a fraction of a pixel that read spreads a pixel-wide lamp speck into its neighbors a little, which a file that clips at white shows as a brighter surround (the bead field's mid-tones read 4.5 levels bright against a sharp render while its specks dimmed); the published answers are a Catmull-Rom read of the history (sharper, not sharp: a speck still spreads, more slowly) and a history clamp fed by the frame's spatial neighborhood (the two-sample spread clamp lets the smear through because the smeared history's own spread is wide), and an edge-aware read that takes the nearest texel where the four disagree beyond their errors puts a positional jitter into a panning camera's floor, so it is not the answer either. *A blurred field of movers at different rates:* under a lens a pixel follows the mean motion of the movers inside its circle of confusion, and where those move apart (the far half of a sweep) the mean fits none of them and the carried light smears along the sweep; refusing the history where they disagree by over a pixel refuses most of such a field and gives the gain back, so the next step is a motion per depth layer inside the circle, or the raster's own depth-of-field split, before any threshold. *Reuse that knows a reflection:* the frames carried across a sequence follow each pixel by its surface's motion, and a polished surface's reflection does not move with its surface (a spinning ball's highlight stays put, a mirror's image moves with the eye and at twice the turn), so on such a pixel the facing test drops the history and the pixel falls back to its own frame's count; the published answer is a second motion vector for the reflected point, the hit behind the mirror reprojected as if it stood there (the virtual-position reprojection the real-time denoisers use for specular history), which would let a slowly turning bead field carry its reflections too, and wants the first hit's reflected distance written beside its depth. *Reuse that the stop reads:* the per-pixel stop judges each frame's own samples, so under reuse a pixel whose carried history is already settled still traces the frame's minimum; a stop that read the carried spread would let a still region of a sequence trace a handful of samples a frame, and would want the history's count folded into the convergence check at the round boundary. The stop has one open end: the rare bright path a pixel has not seen when it is checked, which no per-pixel statistic can count. The published answer past a minimum count and a neighbor rule is a hierarchy over blocks (Dammertz and colleagues, 2010) or a reset of a pixel's remaining count when a late sample falls outside its criterion (the RenderMan 2018 paper), either of which would want a scene that shows the gap first. The bound on twice-bounced light (`--pt-clamp`) answers only the caustic kind of firefly, and the scene of two thousand polished beads mirroring a lit studio is not that kind: its grain is one-bounce light, the studio's lamps through each bead's glossy lobe, where the bound takes 7.5 percent of the frame's light at 4 for a tenth of a decibel, and bounding one-bounce light too (the production tracers' *clamp direct*) takes half the frame, since those beads are their reflections. That scene's grain was the pixel's footprint on a curved near-mirror (two thirds of its per-sample variance, measured with the jitter and the lens frozen) and the point light's highlight through a near-delta lobe, and the tracer now reads the environment and the lights at that footprint; two levers stay open past it. The table's draw is read at the table's own level, since a draw read blurred carries little while the blur's skirt sits in cells the table rarely draws, which lost 13 percent of that scene's light at 128 samples: a table built per footprint level (a chain of tables, the draw taken from the level the cone asks for) would let the table strategy integrate the same filtered picture the lobe does, which is what a polished scene under a lamp-lit studio would want next. And the footprint is isotropic, the mean of the triangle's three edge curvatures, so a surface curved one way (a polished cylinder, a wire) is under-filtered across its flat axis; the published bound takes the largest visible edge curvature instead, at the cost of over-blurring a sphere. Beside it, a light strategy for `.glowing` surfaces, which are credited whole wherever a path lands on them today, so a small hot glowing mesh speckles everything that sees it after one bounce; a mesh light (`MeshMaterial.emissiveColor`) already has one, and the glowing case would join it.
- **Don't 3D-tax the 2D path.** Most sketches stay 2D, so don't make every draw call pay for a depth buffer or perspective divide. 3D is a mode you opt into, not a cost the 2D core carries.
- **Caveat:** the 3D mode itself builds and verifies on a Mac; visionOS needs the visionOS SDK plus a simulator or device.


## Material and scene explorers

The exploring surface is the material explorer example (`Examples/3D/Materials/Explorer`). It puts the curated `Material` library on a preset menu and every finish scalar on a parameter. The stage around the surface switches too: lighting preset, backdrop, fog, shadows, and the preview solid. A caption says when the parameters have left the chosen preset. The rules for anything built on it:

- **A material is source, never a file format.** No JSON and no `.ollinmaterial`: the scene-import rule ("structure becomes source, geometry stays in the file") decides it, since a material is all structure. Live iteration is already `@Param` plus hot reload. Interchange, if ever wanted, means the standard models the loaders already speak (glTF materials, UsdShade), never an invented schema. A preset that bundles surface color and a procedural pattern with the finish (a "marbled" *look*) stays a three-line snippet for the same reason. Only if that want keeps recurring does a value above `Material` earn its own design pass.
- **Reuse before generalizing.** The stage block (lighting, backdrop, fog, shadows) is written per explorer until a third customer proves the shared helper.

## Photorealistic 3D

The realism tier that builds on the physically-based core (PBR metallic-roughness, split-sum IBL from HDRI or the Hosek-Wilkie sky, ray-traced shadows and reflections, SSAO, SSR, raymarched SDF fields) toward the look of a curated product render or film still. Every item is opt-in so 2D and stylized 3D stay untaxed, every one is Metal-native (no engine vendored), and each is implemented from the published technique and credited in `ATTRIBUTION.md`'s Techniques. This is the "anything Metal offers that enriches 3D" tier, after the foundational [3D mode](#3d-mode) buildout: as a new realism technique finds a good fit, it is added to the textured path's twin pipelines (or a gated branch of the surface-mapped fragment, which may branch freely) so non-using materials stay byte-identical.

All Apple-silicon Metal, the realism direction's "build Ollin APIs for anything Metal offers that enriches 3D."

- **A surface's other maps on a traced hit, and a picture's footprint.** A traced hit reads a surface's base-color picture at its full-size level and none of its other maps, so a normal map's relief, a roughness map, occlusion, and a glow map show only on the surface itself, and a fine picture seen small in a mirror can shimmer. The path tracer's per-geometry table already carries all five maps and a ray cone that picks a level; the live trace would read the same maps from its finish records, beside the picture, and the cone it already widens for a rough lobe.
- **A shaped lamp's caustic.** Photons leave a spot light through its cone alone: its IES profile and its cookie shape the direct light (`ollin_apply_light_shaping`) and not the photons, so a patterned or profiled lamp throws the caustic of a plain one. The trace would read the same profile and cookie arrays the lit fragments do, at the emission direction, which is also where the cone is read today. Pinned the way the rest of the chain is (`CausticEnergyTests`): a pane under a cookied spot gives back the cookie's own pattern times the pane's transmission.
- **A mover's edge pixels in the velocity field.** The mover pass rasterizes one sample a pixel while the color is multisampled, so a pixel a mover half covers takes the background's velocity whenever its center falls outside the mover, and its share of the mover's color does not streak. Measured on a 24-pixel box at 20 pixels a frame against the subframe export: with the box's edges on pixel centers the leading side lost about 1% of the box's light, with them on whole pixels 0.16%. A multisampled mover pass resolved by the largest motion (or the motion of the nearest sample, as the depth resolve takes the nearest) would carry the edge; the region invariant in `FrameInvariantTests` is the check, with the box moved to half-pixel edges.
- **A sunlit ground under the sky.** The lower half of a `.sky` environment is the sky's mirror dimmed by the ground albedo, which ignores the sun's beam on the ground. With the sky's sun lighting the scene, a Lambertian ground of albedo 0.3 is about four times as bright as that mirror (7.9 against 2.0 in the sky's unit at turbidity 2 and elevation 0.6), so a face turned down and away from the sun reads dark: the model's facing-to-away contrast is 8.6 over the mirror and 5.0 over a lit ground. A uniform ground radiance, the albedo times the level irradiance of sky and beam over pi, in the sun-lit bake alone, would be the physical floor; the backdrop below the horizon would then turn one flat ground color, a visible change for a scene that shows it.
- **A cloud between the sun and the scene.** Under a cloudscape the sky's sun keeps 1 minus the coverage of its beam, the expected share, whether the backdrop shows the sun in a gap or behind a cloud. A one-texel transmittance toward the sun, written by the cloud march at bake time and read by the lit fragments for the sky's sun alone, would make the light follow the clouds the backdrop shows.
- **The probe grid's resolution.** A surface reads the probe field from the row of probes nearest it, which sits a third of a cell inside the room, so a ceiling reads the floor's lamp pool from closer than it stands: in the invariant room (8 by 4.4 units, a 512-probe grid) that row is about 0.35 units in, and the converged field reads 1.12 to 1.21 of a 64-deep path trace at the ceiling, the pool's falloff over that distance. The bounce's depth is right (the light room's bounce against the dark room's matches the trace's own ratio within 3%); what remains is the grid. A finer budget shrinks it in proportion to the spacing, and a read that corrected the nearest row by the field's gradient along the normal is the cheaper route; `theBounceMatchesADeepTrace` holds the envelope (0.85 to 1.3) until one lands. Two smaller edges beside it: a glowing surface with an emissive map glows at its factor over its whole face in the bounce (the probe trace reads a surface's material, not its texture; binding the per-geometry texture table the path tracer builds would read it), and a smooth metal's bounce is the cosine-convolved field through its lobe, where its sharp image of the room is the reflection pass's.
- **The trace's two reads of an environment.** The continuation's miss reads the environment from a copy blurred to its ray cone (the footprint read that settles a curved mirror's grain), while the environment's own draw reads the sharp map and both weights of the pair are priced from the sharp table, so a lamp's blurred skirt is counted at nearly full weight beside its sharp core. In the bundled interior a matte ball traces 0.475 where the window reads 0.384 and a trace of the continuation alone over the sharp map 0.372, and a metal ball at roughness 0.5 traces 0.434 against 0.379; with both strategies reading one map they meet the reference. A uniform field hides it, which is why the traced furnace holds. The fix that keeps the footprint's gain has both strategies evaluate the same blurred light along each direction, the environment's draw read at the level the continuation would read along that direction (its lobe's pdf folded into the cone as the continuation folds it), judged on the bead scene by its grain and by region means against a sharp reference. The draw read at the cone's level once came up short in a pairing whose two halves priced different lobes, so that measurement wants repeating with both halves on one lobe. `theTraceAgreesWithTheWindowOnABallInALitRoom` pins it.
- **Light behind a solid in the trace.** A shadow ray passes a solid's glass straight, tinted and absorbed, while a continuation that refracts through the solid reaches the light at full weight, so the light behind a solid is counted twice: a 0.3 slab of index 1.5 before a metal ball at roughness 0.5 reads 23% above the continuation's own estimate, and a block of index 1 reads 8% above the bare ball. Two consistent models: shadow rays stop at a refracting solid and its light arrives along the bent paths alone (physically right, with caustics that need samples to clear), or the straight tinted shadow stands and a continuation that refracted after a vertex whose strategies ran picks up nothing (no double count, and no focused caustic). A thin pane is consistent already, since its crossing does not turn the ray. `anIndexMatchedBlockChangesNothingInTheTrace` pins it.
- **2D under the live resolve.** The live temporal resolve runs its 3 by 3 reconstruction over every pixel, jittered or not, so 2D in a 3D canvas softens with the camera still: a hairline reads 85, 219, and 111 across three columns, where the plain frame reads 0, 255, and 0. By reading, every pixel at depth 1, the sky's included, also gets no motion in the resolve, in the upscaler's and the interpolator's motion fill, and in the motion blur, so a turning camera smears its backdrop. `stillTwoDPassesThroughTheLiveResolve` pins the first.
- **The flare's coatings and stops.** `coatedReflectance` weights the inner reflection by t01 squared, where the round trip through the coating is t01 times t10, and blends toward bare Fresnel by cos θ2 off the axis: 0.0086 against the exact single-layer 0.0043 head on into 1.67 glass, and 0.0226 against 0.0069 leaving it at 20°. Two more to measure before a fix, both by reading: stopping down scales a ghost by its passing area alone where held exposure also divides by the stop's area, and `stopped(to:)` sets the iris from the entrance pupil's radius. `aCoatedSurfaceReflectsWhatAThinFilmDoes` pins the coating.

## AR mode and templates (the Meta Spark gap)

Ollin aims to support AR sketches and offer a template-driven AR framework, filling the gap left when Meta Spark was discontinued (January 2025). Apple-only: ARKit plus RealityKit and Metal on iOS and iPadOS, and the visionOS immersive path.

- **It's a mode layered on the phone and 3D/visionOS work, not a separate engine.** AR builds on the phone leg (see [Swift Playgrounds and iOS](#swift-playgrounds-and-ios)) and the 3D camera and depth pipeline (see [3D mode](#3d-mode)).
- **The Spark lesson is templates.** Spark's reach came from ready-made effect templates (face filters, world effects, plane and image tracking) people could start from. The Ollin version: AR-example sketches plus starter templates wired to ARKit anchors (face, world, image tracking), so an AR sketch is "fill in the `draw()`, the tracking is handed to you", the same template-as-on-ramp idea as the examples and `.swiftpm` starters.
- **Caveat:** AR needs ARKit on a device to verify (the simulator has no AR camera).

## iPhone as a sensor array

A Mac has no depth camera, no inertial sensors, and no spare Neural Engine for live perception. A tethered iPhone has all three, so the idea is to let it act as a sensor and on-device ML co-processor for a sketch that still renders on the Mac. The phone captures and perceives, streams the results over the wire, and the sketch reads them in `draw()` as typed values. The Mac keeps the Metal rendering ceiling; the phone fills the input gap desktop hardware leaves.

The starting want was live RGBD point clouds, the kind an Intel RealSense fed openFrameworks before its Mac drivers were dropped. An iPhone with LiDAR is the same world-facing depth-and-color sensor, except the driver is ARKit and Apple maintains it. Both the *recorded* path and the *live USB* path exist (`OllinRecord3D` reads Record3D `.r3d` clips and its live USB stream; see [`Docs/3D/Record3D.md`](docs/3d/record3d.md)): a depth map plus intrinsics unprojects, through the [3D mode](#3d-mode) point-cloud path, into one splat per depth sample (around 49k for LiDAR's 256×192 map, ~300k for TrueDepth's 640×480). The renderer, the unprojection, and the live wire need no new work; what's ahead is *more sensors over that wire*.

**One channel, many typed payloads.** Depth is only the first thing the phone can send. Because both ends are ours and both are Swift, the wire skips the packed-image trick cross-language tools reach for: it is a length-prefixed binary stream of tagged messages, each a typed payload the sketch reads through the same transport-agnostic `FrameSource` abstraction used for the point cloud. The phone app is a configurable sensor hub, so adding a sensor later is a new payload case rather than a new pipeline. A header struct shared verbatim between app and framework carries intrinsics, pose, and a version byte; the rest is raw or LZFSE-compressed channel data.

The phone runs the expensive model on its Neural Engine and ships kilobytes of structured results, so each new sensor is nearly free on the wire:

- **Desktop-impossible.** TrueDepth face mesh and its 52 blendshape coefficients (the Animoji engine: jaw, blink, brow, and smile as 0-to-1 values), 3D body motion capture from a single camera, 6DoF device pose, and the inertial sensors (attitude, gravity, acceleration, rotation rate) a Mac doesn't have at all. The motion stream is the cheapest to build and the most physical: a few floats, no camera, no model.
- **Possible on a Mac, but the phone is the natural host.** Vision runs on macOS too (that's the [`OllinVision` catalog](docs/vision/vision.md)), yet the phone holds the camera and the spare Neural Engine: subject segmentation, scene and object classification, and speech.

Most of those payloads are kilobytes. Only the per-pixel ones (segmentation matte, optical flow field) are heavy, and they arrive as textures, which is what the [layered effects](#layered-effects-and-compositing) graph already consumes, so a live person-matte becomes a `Filter` input rather than a separate feature. Everything here is Apple-native (ARKit, Vision, Core Motion, Core Location, SoundAnalysis, Speech) with no third-party dependency, and it shares its prerequisites with the [phone leg](#swift-playgrounds-and-ios) and [AR work](#ar-mode-and-templates-the-meta-spark-gap).

The transport is the proven live **USB** path (the standard usbmuxd device tunnel): full-quality LZFSE float32 depth plus camera intrinsics and ARKit pose, RealSense-grade, no extra dependency. Ollin's own capture app speaks the same length-prefixed-tagged-payload wire, so the sensor catalog isn't capped by what one borrowed app exposes. Record3D's **WiFi** streaming stays deliberately *out*, correcting an earlier assumption that it would be the easy one: it's WebRTC (a heavy dependency, and the `record3d` library doesn't support it at all), gated behind a paid Extension Pack, and its depth is lossy hue-encoded with no pose: the worst option on every axis, and the reason USB is the only streaming mode supported. Device class gates the sensors (TrueDepth for face, Pro-tier LiDAR for rear depth and scene mesh, A12 and later for body tracking), and several heavy models at once will thermal-throttle, so running every sensor together is a practical limit, not an API one.

**The clean-room stance carries forward to our own app.** Both the `.r3d` file reader and the live USB stream are read clean-room from their public structure, the same constraint our own capture app's wire still works under: the `record3d` library is LGPL-2.1, the same copyleft tier as p5.js, so it stays inspiration only (reimplement the wire protocol, never translating its source, exactly as the sourcing rules treat p5.js). Record3D is credited like the other product references, not vendored. New sensor payloads added over the wire follow the same rule.

## Small pieces

The notes behind the smaller items on the roadmap's *Up next* list.


## Generative geometry

A tier of classic generative-art algorithms that produce *vector geometry* (points, `Contour`s, `Shape`s) and hand off to the geometry that already exists (the triangulated fill, the stroked path, the shape booleans, hatching, SVG export). They're geometry, not rendering, so they live in the `Ollin` core beside the shape booleans and the `Path` builder rather than in a satellite: none needs a heavy framework, and the split `OllinPhysics` took was about keeping simulation opt-in, not about dependencies. All are driven by the existing seedable `random`/`noise` so a result is reproducible, and each ships with an example; several are the natural implementation behind a [recreation](#examples-folder).

The reproducibility note matters: every one of these is a pure function of `seed()` plus parameters, so the same seed always yields the same packing/growth. License stance is the usual one: implement each from the *published technique* (credited in `ATTRIBUTION.md`'s Techniques list). Origin note: these surfaced from mining the `/algorithmic-art` skill's implicit technique vocabulary against what Ollin already ships (Voronoi/Delaunay, the foundational primitive of the tier, is a pure-Swift Bowyer–Watson, no library vendored).

- **A crumpled sheet.** The facets come from the shipped Delaunay triangulation of jittered points over a region (a grid with jitter is the default, and any point set works), and a height function `(Vector2) -> Double` gives each triangle a normal by the cross product of its lifted edges. Each facet carries its shape, its centroid, its area, its normal, and `facing(_:)`, the cosine against a light direction (`lit(by:)` already means lighting a layer by another, so the facet keeps a word of its own). The two cuts are Sutherland-Hodgman (1974) against one plane: `split(along:)` returns both sides, `clipped(toHalfPlane:)` one side, and a band clip is two such clips. They are geometry, so the shape booleans are not involved and the cost is one pass over the outline. The band coordinate the foil filter reads, a projection of the centroid shifted by the normal, is a one-line property, and the facet field is the CPU twin of the filter's GPU facets, so the two agree on the same seed and an example can draw creases in geometry over the filter's fill.
- **Textures drawn as lines.** Wood grain is streamline tracing (a fixed step, an angle read at each step) through an angle field: a base direction, knots as tangent attractors weighted by a Gaussian of distance, and a two-octave wobble; a ring phase, a sine of the distance across the grain with noise added, darkens or drops strokes so the rings appear. The frost needles sprout along an outline's normals with a barb law (barbs at intervals, in a fixed angle range, shortening toward the tip) and a clump factor from noise that lengthens runs of needles. Crackle veins are the shipped `CrackGrowth` seeded inside a cell, with a count set by a crease density and a fork rule at the midpoint. The resist edge is a one-dimensional stepped offset quantized to warp bundles: a flare from one noise, spikes above a threshold of a second, intrusions below a threshold of the first, skewed along the warp. All four are plotter-friendly and reproduce under a seed.

## Technique and algorithm helpers

This is the umbrella the [generative-geometry](#generative-geometry) tier is one wing of: a standing catalog of the creative-coding algorithm vocabulary, added as helpers over time the way the SDF-shape catalog, the `Filter` catalog, and the color family grew. The organizing rule is to route each technique to the seam it naturally belongs on rather than invent a parallel system:

- **Geometry emitters** produce points, `Contour`s, or `Shape`s and feed the existing draw, shape-boolean, hatching, and SVG paths, exactly like the generative-geometry builders. The most plotter-friendly and the cheapest to add.
- **Field techniques** that read per-pixel (a new noise basis, an interference field) land as a GPU `Generator` (a no-input `ollin_gen_*` fragment) or a user shader, reusing the layered-effects substrate rather than the CPU geometry path.
- **Simulations** (iterated function systems accumulated on the GPU) run on the compute and `SimField` paths already in place: persistent ping-pong buffers or textures stepped each frame, drawn through the instanced or image path.

The rest of the catalog follows the same shape: a canonical algorithm implemented from the published source, credited in `ATTRIBUTION.md`'s Techniques list, seeded for reproducibility, and shipped with an example, several of which pair naturally with a [recreation](#examples-folder) (an attractor plot, a flow field, a substrate).

The catalog is also a *Nature of Code* coverage map, so it reads as a familiar curriculum to anyone arriving from that world.

For any animated per-pixel `Generator`, three implementation notes hold (their map is fluid, enonforetsam/fluid, MIT, a single-file WebGL background studio read the LYGIA way: a catalog of which looks matter, implemented from the canonical sources rather than its code): spread a noise-family field's contrast around its midpoint so a palette reaches its end stops (fbm piles up near 0.5 and otherwise hides them); animate by small bounded sways at non-commensurate frequencies so the field churns in place instead of panning as a rigid sheet (which also keeps the time argument precision-safe over long sessions); and normalize field coordinates by the canvas's geometric mean so a portrait crop reveals more field rather than zooming in.

- **Soft bodies that rub, meet rigid ones, and fold onto themselves.** Three contacts a 2D `SoftBody` does not make, each with a place in the solver it already has. *Friction*: each contact in `solveSoftContacts` and each particle held at a wall moves along the normal only, so the tangential part of a particle's motion over the step passes untouched; the position-based friction of Macklin, Müller, Chentanez and Kim's "Unified Particle Physics for Real-Time Applications" (2014) takes it out after the normal correction, all of it below a static limit of the penetration depth times a coefficient and a kinetic share above. *Rigid bodies*: a particle inside a Box2D shape is a query the engine answers (a shape overlap, then its closest point), and pushing the particle out while handing the rigid body the matching impulse at that point makes the contact two-way; the order matters, since the rigid side steps once a frame after the soft side's passes. *Self-collision*: a particle against the non-adjacent edges of its own ring, skipping the few neighbors on either side, which is the pass `keepRingsApart` already makes between the rings of one island, run within a ring. Each wants a pin in `SoftBodyTests` first: a blob that comes to rest on a tilted pile, a ball body that a crate lands on, a noodle folded onto itself.
- **Noise read without the clamp.** Every Perlin form scales its raw field by a contrast gain (2.0 in one to three dimensions, about 1.9 in the looping four and five) and clamps the overshoot, so about 4% of a 2D field sits flat on its ends (1.9% at 0 and 2.0% at 1 over 800,000 samples, 2 to 3.5% at each end in the looping forms, measured 2026-10-16); a curve read straight from it runs flat there. A form that hands back the field before the clamp (the raw value with its own range said, or a contrast the sketch chooses with a soft knee in place of the clamp) is the ask, from a port whose source read an unclamped noise; name not settled. The field's 256-unit period on each axis, the table's size, is documented rather than changed.
- **Points spread evenly over a sphere.** The Fibonacci lattice: point i of n at height `1 − (2i + 1) / n` and longitude `i` times the golden angle, π(3 − √5), which gives every point close to the same area with no seam and no pole cluster (Álvaro González, "Measurement of Areas on a Sphere Using Fibonacci and Latitude–Longitude Lattices", *Mathematical Geosciences* 2010, the half-step offset included). It belongs beside `surfacePoints(on:count:)`, returning `[Vector3]` on a unit sphere a sketch scales, with a hemisphere form for a dome; the even count is exact, which a Poisson scatter is not. A test reads the spread as the ratio of the largest to the smallest nearest-neighbor distance against the lattice's known bound.
- **The bounds index, and space.** A bounding-volume hierarchy over rectangles is built by median splits and refit when boxes move. It answers containment, overlap with a rectangle, and every overlapping pair in one set or across two. It serves picking the shape under the pointer among thousands, the broad phase for outline collisions, and overlap checks while packing. The 3D spatial index keeps the shape of `SpatialIndex` over a cubic grid and a k-d tree. It keeps the exactness rules too: answers agree with a brute-force loop, and ties go to the lower index.
- **Optimal assignment.** The Hungarian method (Kuhn, 1955), or the faster shortest-augmenting-path form of Jonker and Volgenant (1987), finds the pairing with the least total cost. Its cubic cost is fine for a few thousand points. The cost is a closure, so a sketch can match by distance, color, or size.
- **Holographic foil.** A `Filter` sibling of `.liquidMetal`: the layer's alpha is the sheet, and its luminance, optionally, the height. The fragment builds facets of its own (a cellular hash with a constant normal per cell, or the triangulation the CPU facet field uses, so the two coincide on a seed). Per facet it computes the normal's cosine against a light direction, a band coordinate (a projection of the facet's position along a band angle, shifted by the normal's x and y), and a hue from a stepped spectrum ramp at that coordinate, which is why the bands break at every crease; the stripe colors run in grating order, the published grating equation already credited for `.diffraction`. An edge is lit when its direction against the light passes a threshold and dark otherwise, and a vertex whose facets face the light hardest gets a cross glint past 1.0 so a following `.bloom` carries it. `phase` moves the band centers and `lightAngle` turns the light, which is all the animation needs. `Ramp` gains a stepped form (hard stops) for the spectrum and for any poster-flat gradient. The example is a full-canvas sheet: the height function carries a traveling wave so the sheet breathes, the light drifts, and the bands sweep.
- **Chrome for lettering, and interference on a stroke.** The chrome is a signed distance to a warped horizon (a sine of x plus a cosine at a second frequency) inside the layer's shape: a sky ramp above, a dark core band at the horizon, a floor ramp below, a razor break and a bright wire beside it, every stop a parameter. It is the airbrush convention rather than optics, which is why it reads as lettering chrome where `.liquidMetal`'s inflated reflectance bands read as a blob. The interference color is the thin-film reflectance the spectral filter already evaluates, run on the CPU over the `Spectrum` taps for one thickness and index and converted to a `Color`, so a stroke colored by a thickness that varies along it shows a film's true color order.
- **Material textures.** The rust bloom and the verdigris patina share one generator: a blob whose radius is two octaves of noise around a circle, nested copies at shrinking scales in darker tones, an edge drawn as a dark line offset one way and a light line the other (the flaked lip), and blisters as a dark arc on one side and a bright arc on the other; the patina differs by palette and by a noise-gated tide line along the edge. Terrazzo is a tiered dart-throw scatter (Cook, 1986), three size tiers with a separation rule and chips as five-to-eight-gons from a few morph shapes, hatched by size. Bubble wrap is a `Filter`: a hex lattice of lenses, each sampling the layer under it at its center and at an offset, painted as a shadow ring, a shifted magnified blob, two opposed meniscus arcs, and a highlight. The crease kernel is a ridge operator for `Heightfield` and for `.paperTexture`'s folds: a cusp (an exponential of the square root of the squared distance plus a floor), an asymmetric step across the crease, and a flanking trough, applied along any polyline with a width and an amplitude.
- **Six optical and print looks.** Anaglyph (Rollmann, 1853) as a `Filter` over a layer and its depth: the cyan copy shifted one way and the red copy the other by a distance that grows with depth, and where the copies overlap a dark fused tone, which is what reads as solid. Misregistration on the shipped separations: each ink takes an offset and a screen angle, and the plates print over each other, so a black mark rings itself with its own inks. The mirage is a per-row horizontal displacement, noise plus stepped faults above a mirror line and growing sine waves below it, with the picture inverted below the line. Schlieren is the knife-edge method (Toepler, 1864): the gradient of a density field along the knife direction mapped to brightness, light on one side of each spine and dark on the other with a razor transition. The wavelength ramp is a `Ramp` with stops at nanometers converted through the shipped `Spectrum`. Chromatography rings are annular sectors between fractions of a radius, noisy at three frequencies, pushed away from a neighbor's radius with a ruffle, with radial fibers at the edges.

Origin note, the same as the generative-geometry tier: this catalog is the broader read of the `/algorithmic-art` skill's technique vocabulary against what Ollin already ships. This section is the standing list of what's left.

## Expressive brushes and strokes

The fringe expander carries a half-width per path vertex, `StrokeProfile` shapes it by position along the path, a recorded `StrokeMark` drives it from the hand, and a `Brush` stamps a shape along the path instead of expanding it. The tier builds out from there:

- **Painterly simulation stays separate.** Watercolor-style marks built from layered, deformed translucent geometry are a [technique-catalog](#technique-and-algorithm-helpers) recipe over the shape and accumulation machinery, not a brush engine; keeping the two distinct keeps the stroke renderer lean.

## Sound, synthesis, and spatial audio

`OllinAudio` covers both directions: `AudioAnalyzer` (FFT, bands, beat detection) over `AudioInput` / `AudioPlayer` / `Tone` / `Soundtrack` on the listening side, and a polyphonic `Synth` (voices over anti-aliased oscillators, ADSR envelopes, a resonant state-variable filter, delay and reverb) on the playing side, with a clock-free composition tier deciding what it plays. Both sides are routing as a value now, so what is left is depth rather than shape: more of what a voice can be made of.

- **The render-thread rule holds.** Anything the audio callback touches is built non-isolated and allocated before the first note, and parameters reach it through the lock-free event ring rather than a lock. A new voice type inherits that constraint; it is not a per-feature decision.
- **Keep the renderer pure.** The samples are made by a renderer that knows nothing about the audio engine: events in, buffer out, no clock. That is what makes the sound testable at all (a test renders and measures) and what a sound-carrying export would be built on, so a feature that reaches for the engine from inside the renderer is going the wrong way.

- **A tube at the top of its range.** A blown tube leaves its note for the one a twelfth above once the breath passes an edge of its own, and that edge closes in on where the note starts as the pitch rises: measured 2026-10-16 in closing pressures, the clarinet speaks from 0.58 and holds its note to 2.0 at MIDI 57, to 0.80 at 81, and to 0.59 at 93 (the reedy tube 0.55 at 93, the hollow one 0.65 at 87). The dial maps full pressure to `1.1 / closingPressure` as it always did, which is past that edge for the clarinet and reedy tubes from about MIDI 81, so a high note blown hard sounds a weak fundamental under its twelfth. A reed with a mass and a resonance of its own (a second-order reed, whose response falls above its resonance and so holds the lower register) is the published remedy; frequency-dependent loss in the bore is the other. The reedy and clarinet cases of `aDrivenVoiceSoundsItsNote` at MIDI 93 pin it.
- **What the distortion's characters are.** `.softClip` is Apple's Multi-Echo 1 preset, a 357 ms echo at full mix rather than a rounding of the peaks (its parameters read 2026-10-16), so at least one of the five names says something its preset does not do. Each character wants measuring against its name (a sine through it: the harmonics, any delay, any modulation) and, where the preset does not match, another preset chosen or a waveshaper of Ollin's own on the closure unit.
- **An echo longer than two seconds.** `Delay.maxTime` is the 2 s the system's delay unit holds, and a longer time is held there. A looper's echo needs a delay line of Ollin's own on the closure unit, which already carries the convolution and the four motions; two units in series would not do, since each one's feedback loops its own half.
- **Measured and left as they are.** The three tube presets share one steady spectrum at pressure 0.6 (an odd-harmonic series at 1/n, as the reference says of the clarinet), the reedy one thinning only at 0.8; the bowed loudness dips 4 dB at pressure 0.5 inside its 5 to 10 dB range, and below 0.15 the cello drops an octave; `.ponticello` jumps to its second or third mode below MIDI 69, and its pitch wanders by up to 8 cents from note to note, a heavy bow near the bridge being a chaotic regime. A bow whose pressure glides to 0 slows on the string before it lifts, which takes 7 to 12 dB of the ring in the first 60 ms where a stepped lift takes 2. The plucked and struck sources scale their level by the velocity squared where the wave and the driven ones scale linearly (a half-velocity steel string plays at 0.38, a sine at 0.51), a ruling rather than a defect, since nothing promises either.
- **The soundtrack path, what is still open.** Measured and documented rather than pinned: the mixed soundtrack bends anything above 0.7, which a single loud instrument meets too (3,898 of 88,200 samples of a five-note pad chord moved). By reading only: under `--skip` the warmup's notes stay in the clock and the file's sound runs that far behind its picture; a note inside `draw()` is recorded once per settle draw when `OllinApp.exportSettle` is above 1. The offline pan is the system's equal-power algorithm, which puts a source 30 degrees off axis in one ear alone and one directly to the side 18% in the far ear (a 15 degree sweep from 0 to 180 reads 1.00/1.00, 0.49/1.33, 0.00/1.41, 0.06/1.41, 0.13/1.41, 0.19/1.40, 0.25/1.39 and back to 1.00/1.00 behind), constant power throughout; not Ollin's to change, worth a sentence on the page when a moving source is taught.

## Live rigs: physical computing, lighting, and network video

The hardware-and-network interop tier, extending the app-to-app integrations (Syphon, OSC, MIDI, the virtual camera) to the physical rig around the machine. Same posture throughout: play in the existing rig, implement published protocols from the spec, vendor nothing incompatible.

- **NDI.** Network video between machines, the cross-machine sibling of Syphon and the standard of the VJ and broadcast rig. It is the one integration in this tier with no published protocol: the SDK license (the November 2024 NDI SDK License Agreement) forbids reverse engineering the SDK "or any protocols used by the SDK", so unlike OSC, Art-Net, sACN, and MIDI it cannot be written from a specification, and the only path is the vendor's own library. That license shapes how the library can be reached, and three of its terms decide the design:
  - *The headers are MIT; the library is not.* The SDK documentation licenses the header files to open-source projects under the MIT license (each file carries the notice that the MIT terms apply to that file only), so the C declarations can be vendored under `External/` like any permissive component, with their notices intact and an entry in `THIRD-PARTY-NOTICES.md`. The compiled library is under the SDK license alone: redistributable "solely as used by" a product, and only under an end-user license that carries NDI's own list of terms (section 3d: no modification of the SDK, no reverse engineering, NDI's copyright notice, a flow-down asking every third-party developer who builds on the product to accept the same list). A pure-MIT repository cannot carry that list, and a framework cannot pass it on to every sketch, so **Ollin ships no NDI object code, ever**: no library in the repository, none in a release, none in a generated project.
  - *Load the runtime the user installed.* The SDK's own dynamic-load entry point exists for exactly this: `NDIlib_v6_load()` in `Processing.NDI.DynamicLoad.h` returns a table of function pointers, so the satellite opens the runtime that NDI's free installer put on the machine (the folder `NDI_RUNTIME_DIR_V6` names, then the header's redistributable folder), looks up that one symbol, and calls through the table. This is the pattern code completion already uses over sourcekitd (a C interface of our own, nothing linked), with the headers vendored instead of hand-written. Without the runtime the satellite says so once, with the link to ndi.video the license asks for, and does nothing else; the sketch keeps running. The user accepts NDI's terms when they install the runtime, which is where those terms belong. Loading at run time is also what sidesteps two obligations a small project cannot credibly keep, that a product release "must use a version of the SDK that is less than thirty (30) days old" and that a product "must maintain current and complete NDI compatibility at all times": the runtime is whatever NDI's own installer keeps current, and the satellite pins only the load table's version.
  - *The name and the mark.* NDI is a registered trademark of Vizrt NDI AB. NDI asks that a product wanting NDI in its name ask first, that the registered mark travel with the first use in a document beside the statement "NDI® is a registered trademark of Vizrt NDI AB", and that a link to ndi.video sit near wherever NDI is used or selected. So the satellite is named for what it does, `OllinNetworkVideo` with `NetworkVideoSender` and `NetworkVideoClient` (as DistroAV, the OBS plugin, keeps NDI out of its name), the Docs page names NDI® with the notice and the link, and the hosts' about panels carry the statement.

  The practice in open source matches this reading: DistroAV and the openFrameworks addon both carry the MIT headers in their trees and require the runtime installed separately. One caution from the same record: the FFmpeg project removed its NDI device in 2019 over a license dispute with the vendor, so the satellite stays small, isolated, and optional, one a release could drop without touching anything else.

  What it ships, in order: **send first**, the Syphon parallel (`publishNetworkVideo(name:)` over the `frameRendered(_:texture:)` hook, a blit into a shared buffer, and the SDK's video send in BGRA; its asynchronous variant needs the buffer alive until the next call), so a sketch is a source in Resolume, OBS, or vMix on another machine; then **receive**, `NetworkVideoClient(named:)` with `frame` as an `Image(texture:)` and a `FrameSource` conformance so the vision trackers read a camera on another Mac; then audio on the same connection, which pairs with the sound tier. iOS stays out: the SDK for iOS is a static library, which cannot be loaded at run time and would have to be linked into the app, exactly the distribution the terms above refuse. Verification without hardware is a self-loop (send, find, receive in one process, the SyphonLoopback shape) beside the NDI Tools' own monitor, both of which need the runtime installed, and installing it means accepting the EULA, which is @eaviles's to accept, not a session's.

- **A pen plotter, driven live.** Two published command sets reach it. The EiBotBoard firmware in AxiDraw-family plotters publishes a command set for stepper moves and the pen servo over USB serial. GRBL-family machines take G-code a line at a time, with the character-counting flow control GRBL documents, and the program `Toolpath` writes is what streams. Satellites cannot depend on each other, so the driver lives in `OllinSerial` or the port moves into a shared seam in the core. The EiBotBoard moves at the speed it is told, so the driver plans acceleration itself. Pause, resume, and pen height are live controls, and a plotter on the desk is the only proof.
- **Art-Net discovery.** Art-Net 4 finds nodes with two packets. A controller broadcasts ArtPoll, and every node replies with ArtPollReply: its address and port, a short and a long name, the universes each of its ports listens on or sends, and two identity codes, an OEM code issued by Artistic Licence and an ESTA manufacturer code. A node also sends the reply unsolicited when it starts and when its configuration changes. `OllinDMX` sends and decodes ArtDmx only, so today a sketch is reached by a unicast address typed into the console. Both halves are small over the existing `Network.framework` listener and sender:
  - *Being found.* `DMXReceiver(.artNet)` answers each ArtPoll on port 6454 with one reply per group of four input ports, carrying a name the sketch sets (its type name by default), the Mac's address, and the universes `start(universes:)` asked for or the ones heard so far. It sends the same reply once at `start()` and whenever that list changes. The specification has a node wait a short random time before replying, so a network of nodes does not answer at once, and answer the poll's sender rather than the whole subnet where the poll asks for that; both are rules the spec sets.
  - *Finding others.* `DMXSender` gains `availableNodes()`, the house name for a listing that asks the outside world. It broadcasts one ArtPoll, collects the replies for the wait the spec allows, and returns each node's name, address, and universes, so `DMXSender(artNet:)` can be given a node picked from that list rather than an address copied from a console's menu. The reply's decode is total and golden-byte-pinned against the spec's table like `ArtDmxPacket`, and a reply that fails to decode is dropped, never trapped on (the wire harness's rule).
  - *The codes.* The reply cannot go out under someone else's identity. Until Ollin holds an OEM code of its own, the reply carries the placeholder the specification reserves for products in development, and a release never ships it that way: applying to Artistic Licence for the code is the gate, and the ESTA manufacturer code (assigned by ESTA) goes with it. The Art-Net credit line the documentation already carries covers the protocol; the code covers the product.
  - *Proof.* A loopback in one process, a receiver answering a sender's poll, pins the packets. A console or a node on the network is the only proof of the listing and of the sketch appearing in a console's node list, so the item needs a rig on the desk, as the plotter does.

All output paths are real-time side effects of `draw()`, so they follow the render-thread rule (non-isolated closures) and the background-producer handoff pattern the receivers already use.

## New input sources

Each conforms to the core `FrameSource` (so the vision trackers and effect graph consume it) or reads as a simple value in `draw()`.

- **Apple Pencil.** Tilt, azimuth, force, and hover, on the iOS leg where a Pencil exists. Force joins the `pressure` `Sketch` already reads rather than arriving as a second thing, so a sketch written against trackpad pressure carries over unchanged and tilt, azimuth, and hover are the additions; each reads as a plain value in `draw()` beside the mouse and keyboard.
- **Body data, and where the Mac is.** HealthKit (heart rate from a paired Apple Watch) for biofeedback, and Core Location so a `Weather` can follow the machine. Each needs its entitlement and consent prompt, the same "keep the easy things easy, make the prompt explicit" concern as screen capture. Location has a gate the weather reader (`Docs/Helpers/Weather.md`) does not: macOS 14 and later show the Location Services prompt only to an executable carrying an `Info.plist`. A bare `swift run` binary is not even listed under Location Services, and the reference command-line tool had to link a plist into its executable (`-sectcreate __TEXT __info_plist`) to get the prompt back. A plist embedded in the hosts carries a bundle identifier, which may change how TCC attributes their camera, microphone, and screen grants, all of which land on the launching terminal today. So the location reader ships behind a backend seam a test can fake (the Bluetooth satellite's shape), with the plist embedded only in the executables that ask for it, and its first run is a desk check of both the prompt and the other grants. WeatherKit stays out: it needs a paid developer entitlement and a signed app, and the keyless service the weather reads is enough for a sketch. Niche but distinctive and impossible in a browser.
- **Pinch and turn on the phone itself.** The installed sketch's canvas (`OllinMTKView`) turns multi-touch off so a second finger never moves the pointer, and whether a pinch recognizer still sees that second finger is a question only a device answers. So the touch twin turns multi-touch on, keeps the pointer on the first touch by its identity, and reads the pinch and rotation recognizers beside it, each event's change handed to `handlePinch(by:)` and `handleTurn(by:)` the way the Mac's view hands its own. Its first run is a desk check on a phone, since a broken one-finger pointer is the cost of getting it wrong.
- **Typed text on the phone itself.** The iOS canvas view would adopt `UITextInput` and feed the same `typedText()` and `composingText` the Mac's input client fills. The difference is the keyboard: on a phone, becoming first responder raises the on-screen keyboard over the canvas, where a Mac's canvas holds the keys silently. So the canvas cannot claim the keyboard on its own, and a sketch asks for it (a call that raises it and one that puts it away), with a hardware keyboard on an iPad typing without the on-screen one appearing.

## New output surfaces

Ways a sketch leaves the window, beside the flat image / video / GIF / SVG / web-page exporters already in place.

- **Rumble on a game controller.** The motors in a pad are the same kind of output as the shipped haptics tier, reached through `GCDeviceHaptics` and its localities rather than through the trackpad performer, so the pattern layer carries over and only the destination changes. Adaptive triggers and the light bar sit beside it on the same hardware. All of it needs a pad on the desk, since a synthetic controller reports no haptics.
- **The rest of the sketch on the web page.** The platform stance rules out a second renderer, so what crosses is an artifact: the recorded page (`Docs/Output/Web.md`), whose recorder writes down what the renderer received each frame and whose WebGL2 player replays it with the framework's own shaders carried to GLSL ES 3.00 by the shader lexer the GLSL import shares, the import's function table inverted. The core changes by that one recorder and no renderer seam, and every door below keeps it so. The one player part that is not a shader is the stroke and fill expander, the framework's own `OllinExpander` module compiled to WebAssembly and committed as a resource, which is what lets a stroke travel as its points; a door that needs more of the framework on the page takes the same shape (a shared module the Mac draws with, built by a script, its provenance checked by a test), never a second implementation in JavaScript. *The effects that stay with video:* the passes that are a solve or a ladder on the Mac (the diffusion, the summed-area table, the Fourier transform, the alpha-shape design filters, the seamless clone, the depth combines, the flat light) and the simulations with pipelines of their own (the fluid, the watercolor, the self-warp, the multi-scale Turing field); each could cross as a recorded sequence of fragment passes with the intermediate formats the page can hold, a door of its own if a sketch asks for it. *What goes to video:* lit meshes and the rest of the 3D path beyond the raymarched field (a 3D door is a recorded thought, not a plan: a scene export played by a third-party WebGL scene library such as Three.js, its lights and materials mapped onto that library's own, at look-alike rather than exact fidelity and marked so; @eaviles accepts the fidelity cost for the sake of getting 3D sketches out at all, and it stays apart from the exact 2D player), ray-traced reflections, global illumination, caustics, the path tracer, temporal upscaling, mesh fields, compute work such as the particles and the fluids, wide gamut and HDR output, and a sketch whose input is a picture every frame (the camera, a video); the exporter refuses those with the name of the first call rather than drawing a partial picture. *Target:* WebGL2 first, because reach is the point and an iPhone below iOS 26 has no WebGPU, GLSL ES shares Metal's C grammar, the picture is the same, and nothing here needs compute; a WGSL back end comes only when a pass needs compute or WebGPU reach passes WebGL2, sharing the page and the plumbing so it is a second shader target, not a second exporter. *Verification without eyes:* every translated unit compiles in headless Chrome (WebGL compiles synchronously, so a DOM dump reports it), the page played to a fixed frame diffs against the same frame from `OllinApp.image(of:frame:)` under the snapshot tolerance, and the inline fragment inside a bare page diffs against the standalone file. *Parked:* the sketch's own `draw()` compiled through Swift's WebAssembly SDK against a small browser host; it would add a second sketch surface to maintain, and the recorded page with the triangle door covers more sketches. *Boundaries to hold:* this is an export like SVG or video; the `.swift` sketch stays the artifact; the page is generated and never hand-edited and shows no source; there is no share command, no hosting, and no live editor; and no renderer seam enters the core. A browser build of the drawing core is declined, since it would put the Mac-only work at risk, and the bar for it stays where it was.
- **A sketch over the desktop.** A borderless window at the floating level with a clear background hosts the canvas. Clicks pass through where the sketch drew nothing. The window turns `ignoresMouseEvents` on and off from the alpha under the pointer in the last frame. A hit-test closure from the sketch can decide instead. A global pointer monitor keeps the answer current while events pass through. It sits beside the wallpaper and the menu-bar strip as a host the framework provides. The canvas keeps its alpha through the present pass, as the transparent export does.
- **A second window.** The runner draws one sketch into one view. A second window needs a second draw entry with its own canvas size and camera, sharing the sketch's state. The room joins several machines, and installation displays split one canvas across outputs. This is the case between them, several pictures of one state in one process.

## Rendering and color frontier

Deeper use of the Metal core and Apple displays, every item opt-in so the 2D path stays untaxed.

- **Dolby Vision.** The static HDR10 metadata an extended sketch exports describes the whole file; Dolby Vision carries per-scene metadata instead, which a generative piece could drive from what it knows it just drew. It needs the licensed encoder path rather than AVFoundation's plain HDR writer, so it is a licensing question first and an API question second.
- **One vertex a particle.** A particle draw is an instanced quad, six vertices a particle, and that vertex work is most of what a spray costs: of a 43 ms frame scattering four million points a frame at 1080 square (measured headless in one process with the variants alternated; the GPU's share does not depend on the build), the scatter kernel is about 3 ms and the draw about 24 ms before any fragment, the same 24 ms with every particle degenerate. Drawing each particle as one point primitive (`[[point_size]]`, the fragment reading its offset from `[[point_coord]]`) would cut that vertex work by six, which would roughly halve `LineSpray`'s frame. Metal caps a point's size at 511 pixels, so a light particle past that keeps the quad, and the spray knows its own point size on the CPU, so it can pick the path per draw; a user kernel's sizes are not known there. A strip of four vertices instead of a list of six is the small first step. Both deposits (the multisampled one-texel quad and the single-sample bilinear splat) have to land where they land now, to the byte or within the snapshot gate.
- **An accumulator of ink in single precision.** A frame's pass of light particles alone is single precision; a pass that also holds a shape, an image, or text keeps the half-float multisampled pass every layer has, because only the newer Apple GPUs (Apple7 on) multisample a 32-bit float format, while all of them blend it. A pass that piles thousands of faint shapes onto one pixel in a single frame loses most of them, since half float stops adding a sample once the pixel holds one to two thousand times what it brings. Where the GPU multisamples the format, such a pass could take single precision at twice the memory of the half-float one; on the others it stays as it is.
- **The stylized sheens in a traced frame.** The tracer reads a material's metallic-roughness base, its transmission, its thin film, and its emission; the iridescent rainbow, the sparkle, the rim glow, the subsurface glow, the toon bands, the gooch ramp, and the standard finish's own highlight all trace as a matte surface at the raster brightness, and the page lists which library material loses what. A traced form for each: the iridescence and the rim are view-angle tints that fit on the specular Fresnel term as a weight; the sparkle is a glint lobe, a stochastic microfacet distribution whose flakes flash at the sampled angle; the subsurface glow is a thin-slab transmittance read at the hit; the toon bands and the gooch ramp remap the first hit's diffuse term only, since a stylization is a picture of the light rather than a transport of it, and a bounce off a toon surface still carries the plain albedo. The standard finish's highlight is the raster's own Blinn-Phong lobe, which fits as a sampled lobe beside the microfacet one at the same normalization the lit fragment uses.
- **`displayHeadroom` under a window that asked for none.** By reading, unmeasured: `displayHeadroom` reads the screen's headroom every frame for a `.standard` or `.wide` window too, where its page says 1 for a window that asked for none.
- **The live soft proof on a wide canvas.** By reading only, unmeasured: the live soft-proof filter indexes a lattice baked for `from: .displayP3` with components in sRGB primaries, so a wide canvas proofed live may read each color through the wrong lattice node. A probe of one saturated P3 color through `Filter.softProof` against `Image.softProofed` decides it.

## Authoring and editor tooling

Editing experiences the live-reload core makes possible, and the place Ollin states its line on AI.

- **The boundary, stated first because it is load-bearing and public.** The README promises it ("It's a tool for making art ... Ollin is not a generative-art model"), and it extends to any AI *feature*: AI helped build the framework and plays no part in the work made with it. Not as an author of a sketch, and not at the controls either. The tidier line, operate the framework but never author the work, does not survive contact with generative art: the parameters are the work (a palette, a density, a speed chosen by hand is the hand), so a model moving them toward a phrase is a model making the piece; it cannot tell a development nudge from an aesthetic choice; and the exception it forces into the README's promise costs more than any such feature is worth. Ruled 2026-09-07 over a working slice (`dbb93dbc`, the on-device model moving `@Param`s toward a phrase, reverted the same day; it also moved parameters the words never named). What stays: models that read the world (the perception tier, depth, listening) are input the way a camera is, credited in `THIRD-PARTY-NOTICES.md`, and never draw. A proposal that puts a model between the artist and a value, a color, or a picture is out by this ruling, not parked.
- **No node editor.** The effect graph (`compose` / `Filter` / `Combine`), the SDF combinators (`SDF` / `SDF3D`), and the `Visual` chains are graphs written as code, which makes a node surface look cheap: a presentation of a structure that already exists, round-tripping to Swift. In practice it needs an interface of its own (selecting a node, editing its settings, wiring it), and once the work can be done in boxes it moves out of the sketch, which is the node-graph studio the scope rule turns away. The graphs stay code, and `@Param` already gives their values a control. A code-less, node-based Ollin could come one day as a separate app built on the framework, taken on for real demand and never as a core feature.
- **A reload refused when the framework under it changed.** A live host loads a sketch's dylib with `-undefined dynamic_lookup` against itself, compiled against the module files it finds beside its own binary. When a build replaces those files while a window runs, the next save compiles the sketch against a framework the running host is not, and the load succeeds and then crashes. A peer lost two windows that way on 2026-10-16: a release build added a stored field to `Shape`, the reloaded sketch died in `swift_release` on its first draw, and a relaunch against modules still being written died inside the shape's lock. The host can tell before it loads. Record the framework modules' identity when the host starts (the `Ollin.swiftmodule` files under the discovered `-I` directories, by modification time and size or by a hash), check it before each compile, and on a change refuse with one sentence ("the framework was rebuilt since this window opened; relaunch") while the running sketch keeps playing. Every host goes through `SketchLoader`, so OllinLive, OllinLiveCoding, OllinRun, and the gallery get it at once.
- **The rest of a package in the live window.** A sketch file in a package target compiles with the target's other files (`SketchLoader.Scope.target`, `SketchTarget`), and the compile reaches the framework and its libraries because the host carries them; anything else the package resolves does not compile live. *Other targets of the package:* build them first with `swift build --target <dep>` against the package's own `.build` (the read already knows the dependency names from `dump-package`), then compile the sketch with `-I` on the built modules and load the dependency's object code beside it, either linked into the sketch's dylib or as a dylib of its own loaded first. Two questions decide the shape: a dependency built as a static archive drags its own copy of anything it links (Ollin above all, the two-copies failure the host's `-undefined dynamic_lookup` exists to avoid), so the dependency's link must leave Ollin unresolved too; and a save to the dependency's own files has to rebuild it, so the watch covers its folder. *Other packages:* the same, after `swift package resolve`, with the build's lock now in play, since the dependency build has to happen in the package's `.build` or a copy of it (the manifest read's private scratch folder avoids the lock only because it builds nothing). Measure the first compile and a save before choosing: a dependency rebuilt on every save would cost a live session its reason. *The phone route* (`PhoneProject`) references the one sketch file and its assets; a target's other `.swift` files would join the spec's sources the way the assets do, read through the same `SketchTarget`, and the tether's watch would follow the target's folder. *Completion in the live-coding editor* hands the service the buffer alone, so a helper's names do not complete; `completionArguments(sourceFile:)` would add the target's other files, from the manifest cache only, so a cold read never holds the editor up. *A shape drag in a helper file* is refused by name (the drag host owns one file, `ShapeDragHost.sourceName`); a host that answered for every file of the target, by the site's file name, would let it write the helper.

## Collaboration and multi-device

Apple-native, low-ceremony ways several machines share one piece.

- **SharePlay co-creation.** Not a feature but a transport: the room's session logic (membership, the shared clock, last-writer-wins parameters) is transport-independent behind `RoomTransport`, so the whole idea is a GroupActivities-backed transport plus the app entitlement. The constraints that shape it: the `com.apple.developer.group-session` entitlement applies to apps only (macOS 12+), so it reaches a sketch bundled as an app and never a terminal-launched one; the session context is system-owned (two Apple IDs on a live FaceTime call) and both Macs need the same app installed, so parameter, seed, and clock sync work while co-editing the code cannot (an edit is a rebuild and a redistribution); `GroupSessionMessenger` is a small-payload channel (reliable and best-effort modes), which fits the room's wire and nothing heavier; and a proof needs two Macs, two Apple IDs, and a real call, so no CI holds it. One check to run before any code: whether the entitlement is available with Developer ID signing outside the App Store. Gated on a public repository, a verified real-signing run for the app kind, and demand.

## Learning: the Guide

The narrative-learning tier's design home is the [`Guide/`](guide/index.md) tree itself: the writing rules and pedagogy live in [`Guide/AUTHORING.md`](https://github.com/eaviles/Ollin/blob/main/Guide/AUTHORING.md), and the chapter queue, per-chapter briefs, feature-coverage matrix, and roadmap parking lot in [`Guide/PLAN.md`](https://github.com/eaviles/Ollin/blob/main/Guide/PLAN.md). Design decisions about the Guide go in those two files, not here.

## Opening the repository

Everything a private repository never had to answer for arrives at once when it opens. This section covers how Ollin presents itself to somebody who has not seen it before, which is a different question from what it can do. The observations below came from reading the public face of vgpu (credited under Directions ahead in [`ATTRIBUTION.md`](attribution.md)) against ours, so the sizes and ratios are measured rather than guessed.

- **The README does three jobs, and the split between them is what a presentation pass protects.** It pitches the project, states positions (Apple-only, the AI posture, licensing), and points at the catalog, which lives at the top of the reference index because an index is where an enumeration reads well and a front door is not. The pitch stays above the fold, and the position statements are load-bearing enough to keep but not to lead with. The opening as it stands (title, one line, a nav row, the facts list, the status callout) is the part that already works, so the pass named under Toward 1.0 should protect it rather than rewrite it, and anything that grows the file past a pitch should ask which of the three jobs it is doing. Applying that question once (2026-09-19) found two sections doing a fourth: *Live reload* and *Export* had grown into a manual, with flag-level and option-level detail the pages they link already carry, so the claims stayed and the usage went back to those pages. Four headings are load-bearing beyond the README itself, because the website's front page names them (`SiteHome.rows`: *Hello, circle*, *Run it* and *Install* side by side, *What's in it*), and the run command under *Run it* is the one the site rewrites per sketch, so neither can be renamed casually. The remaining question for the pass at the tag is *Documentation*'s nine-item starting list, which is a curated route into the reference and reads as one, but sits under a paragraph that has just called `Docs/README.md` the annotated index.
- **The inward and outward tiers are wildly out of proportion, on purpose, and worth watching anyway.** `CAPABILITIES.md`, `ARCHITECTURE.md`, `CLAUDE.md`, and this file come to roughly 1.3 MB of contributor and agent-facing prose, against a 47 KB README. About 28 to 1. Each of those files earns its size (`CAPABILITIES.md` exists because `CLAUDE.md` outgrew the context budget it has to fit in every turn), so this is not a cleanup task. It is a reminder for the release pass that the 40 KB is the part anybody outside will actually read, and that `ATTRIBUTION.md`, unusually thorough for a project this size, is one of the better arguments Ollin can make for itself.
- **The agent-facing question, stated so it can be decided rather than drifted into.** What is left of it is one convention: the reference served over a tool protocol, from the same pages the CLI prints and the site writes as an index. What is not settled is whether Ollin wants it, and the reason to decide deliberately is that the project has a public position on AI which this touches. The distinction that seems to hold: serving accurate reference to an assistant so a person writes their own sketch is the same act as serving it to a search engine, while anything that produces sketches is the thing the boundary refuses. The practical argument for is that an assistant working from guesses invents API and the person blames the framework, and it is stronger here than for an established framework, because no model has read an API this new. The argument against is that the gesture reads as endorsement whatever the README says. The fact that keeps the question cheap to leave open: a general-purpose documentation server already reads an index of the shape the site writes, so a server of Ollin's own buys a search and fewer round trips rather than access to the reference at all. Both arguments are real, so this stays a question with @eaviles's name on it.

## A third-party extension ecosystem

The contributed-addon ecosystems are a large part of why p5.js, openFrameworks (`ofxAddons`, the `ofx*` convention), and OPENRNDR (`orx-*`) have the reach they do, and it's the one incumbent advantage no Ollin *feature* closes. SwiftPM makes the mechanics free: a third-party Swift package that declares `Ollin` as a dependency and adds types or `SketchExtension`s is already a working extension, installed by URL like any Swift package. So the work is not plumbing, it is **convention and discoverability**, and what remains is the discoverability half:

- **A curated list.** An awesome-list-style index so extensions are findable without a registry of our own. It needs published extensions to list, so it follows the repository going public rather than leading it.
- **Ollin's own front door.** The listing on the Swift Package Index. It is a pull request against the index's own package list, which is the one step that cannot be taken from inside this repository, and it is worth taking deliberately: the listing carries a compatibility matrix, and the rows for the platforms this package does not support read as red rather than as absent, which is correct and still worth expecting.

Two facts about the surface constrain anything built on this tier, and both are easy to assume the other way round. `Drawer` is internal, not public, so an extension extends `Sketch` and the public value types rather than the renderer's recorder. `Filter` / `Generator` / `Sim` / `Combine` are structs with no public initializer, so the effect catalogs take no new cases and the way into the render graph is a `Shader` through the `.shader(_:)` factories. Opening either would be a deliberate API decision, not a small one. The current seams are cataloged in [`Docs/Tools/Extensions.md`](docs/tools/extensions.md).

License hygiene carries over: a third-party extension is the author's own package under their own license, not bundled, so it sidesteps the vendoring rules entirely.

## Accessibility and inclusive text

Inclusivity on the platform's native support. What ships is in [`Docs/Helpers/Accessibility.md`](docs/helpers/accessibility.md); one decision from it carries forward, which is that nothing is applied on a sketch's behalf, since only the sketch knows which of its movements, distinctions or shapes is load-bearing. One idea is on record as deliberately out, and the reasoning is what guides the next person who proposes it:

- **Descriptions Ollin could derive.** A sketch writes its own words today. The vector recorder holds one frame as typed commands (`SVGCommand`). Shape kinds, positions, sizes and colors are all there, so that is the seam a derived summary would be built on. It stays out for now: a list of what was drawn is not a description of what it means.

## Testing

What the first coverage read (2026-10-10, `Scripts/test.sh coverage`; `Tests/README.md` § *Coverage, and the hosts' own gates*) left without a test, by class, with why. 83.2% of 186,385 lines ran; of the 31,224 that did not, these are the classes, largest first.

- **SwiftUI views** (the inspector, 1,587 lines; the timeline panel, 963; the host windows, the cues card, the axis widget, the generator's stages; about 7,500 lines in all). A view test needs a driver: the CGEvent window driver the hosts were checked with by hand is the ground, and a harness that clicks a control and reads the parameter it moved is its own slice.
- **Device and network backends** (Core Bluetooth, the phone and Record3D devices, the room's local-network transport, the laser DAC's live socket, the WebSocket push transport; about 1,500 lines). Each is tested through a stand-in at its seam; the live half needs the device. The WebSocket transport is the exception: a local server through Network.framework would reach it from a test, as `PushFeedTests` reaches the event stream.
- **The command fronts** (`ollin docs`, `ollin new`, the tether, the snippet and figure tools; about 3,800 lines). Scripts drive them (`check-snippets.sh`, `guide-figures.sh`, `preflight.sh`), so they run on every relevant commit without a test reading their lines.
- **Live-only paths**: the live window's own half of `SketchView.swift` (1,930 lines: the window modes, AppKit's events, the display link), `startRecording(to:audio:)` (the live loop's take), the environment downloads (the network), the Vision and sound models' live runs (a camera, a model); their failure sentences are now held by `VisionStatusTests`.
- **A path the desk never takes**: the raster point-light shadow (`encodePointShadowPass`) runs only without ray tracing, so a desk read reports it unreached while the CI runner takes it every run.
- **Public forwarders whose core call is tested**: the 3D draw helpers (`drawPyramid`, `drawHelix`, the Platonic solids, `drawSweep`, `drawLathe`, `drawExtrude`, `drawStrip`), the `no…` switches (`noShadows`, `noFog`, `noCaustics`, ...), the quality setters, the camera rig's calls, the combinator scopes (`subtract { }`, `chamferUnion { }`, the stairs and columns families), the text metrics, and the GPU simulation factories. The 2D forms now have `BareFormsTests`; a 3D twin of it (each helper against `drawMesh` of the same `Mesh`) is the natural next.
- **Small public values not yet held**: the variable-font axes (`OutlineFont.weight`, `width`, `slant`, `opticalSize`), single-stroke fonts (`StrokeFont`), `@Smoothed` and `@Sprung`, `LSystem.isStochastic`, the parquet deformation's profiles, `drawEnvelope` and the caustic of a ray family, `Table.Row.bool` and `color`, `Automation.write`, and the `Complex` operators. Each has an analytic oracle; a session of value tests would take them together.

## Examples folder

The `Examples/` set is maintained: small, runnable sketches that double as the "learn it in an afternoon" on-ramp and the compile-tested showroom (current status, the `OllinExamples` gallery, and CI are described in [`CLAUDE.md`](https://github.com/eaviles/Ollin/blob/main/CLAUDE.md)). The design decisions worth keeping, because they guide *extending* the set:

- **Layout organized by topic.** Group by feature so they read as a learning path: basics, color, motion, patterns, text, and so on. Each example is one minimal, focused sketch that shows one idea well. The top `Examples/README.md` indexes only the categories (a table linking to each category folder); each category folder carries its own `README.md` listing its sketches, so the front page stays small as the set grows.
- **Per-example folder, generic `Sketch.swift`.** Each example lives in `Examples/<Category>/<Name>/` and its file is always named `Sketch.swift` rather than `<Name>.swift`, so the example's identity lives in the folder name, not a repeated filename. The per-example folder is also each sketch's home for its own assets (fonts, images, shaders sit alongside `Sketch.swift`), which is the reason not to flatten the layout.
- **The single-file `@main` mechanism.** SwiftPM allows only one entry point per executable target, so each example is its own small executable target (scripted as the set grows, rather than hand-maintained). Adding `static func main()` to `Sketch` on the core (`extension Sketch { static func main() { OllinApp.run(Self()) } }`, which needs a `required init`) makes a file self-contained, so an example is just `@main final class HelloCircle: Sketch { … }` with zero boilerplate. Built once on the core, so any single-file sketch flow (examples, scripting, embedding) can reuse it.
- **A feature isn't done until it has an example.** Each new primitive or capability ships with one. Examples are also a forcing function for API quality: if the example is awkward to write, the API needs work.
- **Snapshot coverage grows with the set.** Compile-testing proves an example builds; render-correctness snapshot tests (`Tests/OllinTests`) prove it still renders the same: a sketch is rendered headlessly through `OllinApp.image(of:)` and its pixels diffed against a committed reference. A new primitive earns a small, deterministic fixture and a recorded reference, so the catalog stays regression-protected as it grows. The reference is GPU-specific (anti-aliasing differs across GPU families), so references are recorded on a developer's machine (`OLLIN_RECORD_SNAPSHOTS=1`) and the comparison stays a local gate, kept out of CI; keep new fixtures flat and pixel-friendly so the mean-difference tolerance has room. A peer Swift and Metal framework does per-primitive snapshot tests the same way (a frame hook gated behind `#if canImport(XCTest)`, using pointfree's swift-snapshot-testing), a reference if richer image diffing is wanted later.
- **Reuse the samples elsewhere.** Examples can seed the Swift Playgrounds `.swiftpm` starter and serve as starter templates for new sketches. Author once.
- **A candidate list read off a peer gallery.** The vgpu example gallery (MIT, Vercel Labs; credited under Directions ahead in [`ATTRIBUTION.md`](attribution.md)) was read the way the shader catalogs are read, for which pieces are worth having rather than for code, since it is WGSL against WebGPU and nothing in it transfers as source. Most of it maps onto capabilities Ollin already ships (a fluid solver, instanced draws in the hundreds of thousands, a recorded batch, an environment map lighting a scene, an SDF clip through a mesh, depth estimation and classification through Core ML, a hand-tracked canvas), so those are covered and want nothing new. One is the natural first reader of a shipped capability, a **light-painting scene** over the light a flat sketch works out for itself, and three wait on the tiers above: an **ocean surface** for the [2D transform](#technique-and-algorithm-helpers), a **glass object standing in a drawn scene** now that it has something to stand in, and **one scene shown through each anti-aliasing path at once** for the [post-process pass](#rendering-and-color-frontier), which teaches the four-way split better than prose does. Two of the gallery's pieces are its own and a sibling product's branding, so they stay out. The license permits redistribution, but a rewrite in Metal from the same published techniques is what will actually happen, so each one carries the per-example header naming the gallery entry as inspiration alongside the technique's canonical source, per the sourcing rules in [`CLAUDE.md`](https://github.com/eaviles/Ollin/blob/main/CLAUDE.md).
- **Recreations grow by artist, and double as a build-it-to-find-the-gap forcing function.** The `Examples/Recreations/` set (organized by artist, homage framing, per-sketch provenance; see its README and the sourcing rules in [`CLAUDE.md`](https://github.com/eaviles/Ollin/blob/main/CLAUDE.md)) includes Vera Molnár, Georg Nees, Frieder Nake, Bridget Riley, Osamu Sato, Nick Cave, Jared Tarbell, Manuel Felguérez, Manfred Mohr, Josef Albers, John Whitney, Sol LeWitt, Casey Reas, Vicente Rojo, Ryszard Winiarski, Hassan Sharif, Owen Schuh, Monir Shahroudy Farmanfarmaian, Gego, and Vladimir Bonačić. Candidate additions, and what each would exercise:
  - **Inspired-by only (living/commercial: credit, never port source).** **Tyler Hobbs** (*Fidenza*, flow fields, which the GPU `Particles`/`CurlField` path already renders at scale) and **Anders Hoff / inconvergent** (differential growth). These pair with the [generative geometry](#generative-geometry) tier: Hoff to differential growth, Hobbs to flow fields. (Candidate list sourced from mining the `/algorithmic-art` skill; it names no artists itself, so these map its technique families to the canonical figures.)

## Developer experience: Swift-idiomatic ergonomics

A standing track, not a one-time task: keep mining Swift idioms to make the API read better than a literal p5 port would. "Feels like p5" is the shape (bare calls, terse positional args, motion by default), but Swift's type system lets the same call carry more meaning with no extra ceremony, and that's a differentiator worth pressing. The exemplar to extend: `canvasSize` is a `CanvasSize` enum (`.square(n)` / `.size(w, h)` plus named presets like `.uhd4K` / `.vertical1080`, with `.portrait` / `.landscape` to flip orientation, in `CanvasSize.swift`): it reads as English, is discoverable by dot-completion, carries integer pixels as it should, and beats a bare `createCanvas(3840, 2160)` on both clarity and correctness. More of that. Candidates, in rough order:

- **What the second API meaning pass found and left (read 2026-09-19, per satellite), after the two passes that acted on it.** The renames it made are in the changelog; the rule it settled, one shape per kind of failure, is in [`CLAUDE.md`](https://github.com/eaviles/Ollin/blob/main/CLAUDE.md), and the failures it found are one shape each now. Three rulings came out of the work and are worth keeping: a start a sketch calls itself throws for what it can see, as `Camera.start()` and `AudioInput.start()` do, while a start the first note makes for the sketch (`Synth`, and `Tone.play()` / `AudioPlayer.play()` beside it) writes `unavailableReason`, since the note has nobody to throw to; a tracker's `detect` throws `VisionError.unavailable` with the same sentence its `unavailableReason` holds, because a call in hand can throw where a frame drawn without one can only read, and that is two moments rather than two shapes; and a server that fails to bind keeps the property and says the sentence once on stderr too, the way the vision status does, since the terminal is where the person who typed the port is looking. **Two words for one answer:** `unavailableReason` (the Bluetooth, haptics, controller, vision, camera, audio, Syphon, Link and server types) is a present state and clears when the thing works, where `lastError` (MQTT, the laser, the serial port) is the last thing that went wrong and is cleared by the next success; they are two answers and stay two words. **Left in place, with the reason:** `Collider3D.capsule(height:radius:)` takes its two numbers in the opposite order from `drawCapsule(radius:height:)`, and so do `cylinder`, `cone`, `taperedCapsule` and `taperedCylinder`, which makes the standing colliders one height-first family; matching the draw call would mean flipping five cases and thirty-three positional pattern matches, several of which bind two `Double`s that would swap in silence, for one label order, and the family's own consistency is worth more than agreement with the draw call. `PhoneDevice.isRunning` and `Record3DDevice.isRunning` report the socket rather than the retry loop, which is a behavior question before it is a naming one; `PhoneTouches.isReporting` only ever rises; `Hit3D.body` is typed `any Colliding3D` where `Contact3D` calls the same value a thing; `SoftBody3D.density` moves nothing but buoyancy while `Body3D.density` sets mass; and `Character3D` carries three velocities of which two are intents.
- **Keep the presets pattern growing.** The orientation-flip trick (`.portrait` / `.landscape` as computed properties on a value type) generalizes: look for other places a bare tuple or magic number could become a named, dot-completable value with helpers.
- **What the long-run pass left (read 2026-09-26, beside what it fixed).** *A frame's image batch whose texture never builds* is left out of `FrameProfile`'s counts by design, and no test draws one. *The widget run answers a renderer that will not start with an empty run*: `widgetFrames` is what the system surface asks, and an empty timeline is the one answer it can show, so the failure is silent there (the `--export-widget` path throws on an empty run); a reading the widget's own host could show is not built.
- **What the test audit left (read 2026-09-26, reread 2026-10-06).** Three things stay open. *About sixty files read a frame inside a helper of their own* that reads and then computes (a luminance sum, a histogram, a peak); each could call the shared `pixels(of:)` for its first half (`Tests/README.md` § *What the suites share*), one file at a time as it is touched, and none is worth a sweep. *Three skips are judgment-shaped*, named in their comments: the two listening tests that read what the classifier made of a synthesized tone, and the one where an empty transcript is a recognizer with no on-device model; a probe for those would be the test itself, so they keep their early return. *The web families' sanity tests* (a page against the wrong frame must read as different) keep a browser launch each, six in all, since each proves its own family's gate can fail; they could join their family's batched launch only by giving up that independence. One item the audit named is not ahead: the GPU simulation probes stay at the default canvas, because the attractor flow, fluid, soft body, drift, particle life, and spatial-hash suites take 6.2 s together at 1080 square (measured 2026-10-06), so a smaller canvas has nothing to cut.
- **What planting the wildfire left (read 2026-09-29, beside the forest fire's inject).** *Two rims make a mark*: the snapped injects ignore coverage under three quarters, and two one-texel SDF rects beside one gap composite to exactly that, so a dense grid stamped a rect a cell fills its gaps. A stamp drawn into a state field without the analytic halo would let a sketch plant cell by cell; today the answer is an outline or a polygon, whose coverage is geometric.
- **What the same-seed pass left (read 2026-09-26, beside what it fixed).** *A reflection ray on a tie*: a ray that meets two surfaces at one distance, a cylinder's base cap lying on the floor's top face, gets whichever the intersector reaches first, and that follows the acceleration structure the driver built for that process, so one pixel of a same-seed export came back half a percent different about one run in three, and no other pixel moved (found in the Upscaling example by float exports, which see a difference the 8-bit picture hides unless it crosses a level; it went with `rayTracedReflections()` and vanished with the bases lifted a thousandth). A rule that makes the tie deterministic needs the structure built non-opaque and every candidate compared by distance and then by primitive index in the query loop, a cost on every traced ray, reflection, shadow, and thickness alike, so it waits for a scene that shows it at more than a pixel; the example stands its geometry a hair above the floor instead, and says why. The milestone of 2026-09-30 found a second instance in the Convertible recreation, mirror and painted-glass plates on a plaster kite under `rayTracedReflections()`: one pixel two levels apart at (616, 699), one run in three, the rest of the picture the same. *A lit face one level off* (reported 2026-10-15, larger than either): two quiet `--export-sequence` runs of one scene with global illumination, a directional light's shadow map, the export's temporal passes, bloom at threshold 0.7, and feature edges on 1,024 meshes differ on 12 of 35 frames, every difference exactly one 8-bit level, on 1 to 15,018 pixels, speckled along the key light's shading gradient on a bright face and never on the inked edges or the haze; three `--export --frame 26` runs in each of six configurations gave 16 identical and 2 not, with the volumetric beam off and with the flare off, so neither is needed. No example combines these, so `check-determinism.sh` has never seen it. The way in is a failure rate from many renders per configuration, then global illumination, the shadow map, the temporal passes, and bloom turned off one at a time; the suspects are a sum taken from many threads at once (an atomic), a pass that reads an attachment it also writes, and an uninitialized read near a rounding edge. The smallest scene that shows it joins the determinism check. *A detector that is not exact*: Vision's trajectory request fits its arcs a few millionths differently from run to run, on identical frames with identical timestamps (measured on a fixed synthetic throw, and in `TrajectoryTracking`, where two of five exports at frame 900 detected a different set of arcs). The export clock makes every input repeat, so the rest is inside the request; a trajectory detector of Ollin's own would be exact, and waits for a sketch that needs one. *A detector answering an input during a replay*: an export analyzes every frame its sources offer on its own clock, but a detector a sketch calls from an input hook (a `PointSegmenter` pick on a click, a `Task` awaiting `detect(in:)` on a finished stroke) answers on a background task, so a replayed take that clicks has the answer land on whichever frame the machine reached. Under an export such a call could wait for its answer on the thread driving it, as the trackers do. No example records a take like that, so the change waits for the first one that does. A tracker made in a property initializer, before the export begins, can also read a frame the still feed's thread published during a slow `setup()`; it shows only for a tracker whose answers carry from frame to frame, and every example makes its trackers lazily or in `setup()`, where they never see one.
- **More idiom candidates.** Trailing-closure scoping beyond `withState { }` (already the model, as is the `drawShape { p in … }` path builder); `@dynamicMemberLookup` only where it genuinely clarifies. The richest seam so far has been **conforming Ollin's types to the protocols the standard library already understands**, since that hands a sketch the whole library rather than one new call: a type that is a sequence of things says so, and a type that generates randomness is reachable wherever `using:` is asked for. Look for the rest of that shape (an ordered set of values that still hides behind a stored array, a value with a natural stride or ordering) before inventing spellings. Two literal conformances were weighed and left out on the bar below: `ExpressibleByArrayLiteral` on `Palette` loses the type name that `Palette(.red, .white)` already carries, and `ExpressibleByIntegerLiteral` on `Color` would let any stray integer become a color, which is shorter and less clear. Four collection candidates went the same way, and the reasons generalize to the next one: `Path` keeps pen *commands*, not points, so the element a collection would hand back has to be sampled and a subscript would re-sample the whole curve to answer for one point; `Rhythm` would arrive with `count` saying exactly what its own `length` says, over a run of `Bool`s nobody walks, while `onsets` is the read people actually want; `Ramp` has a `reversed` *property* returning another ramp, which a `reversed()` method returning a list of stops would sit one pair of parentheses away from, meaning something else; and a document carrying several parallel lists (`Automation`, `MIDIFile`, `Timeline`) has no single list that is "the" content, so picking one to be the elements is a coin toss the reader has to remember. Two shapes to watch for on the way in: a conformance that quietly redefines an existing call (a geometric `contains` becoming a membership test), and `Sequence` shadowing the global two-argument `min`/`max` inside the type's own extensions, which is a compile error at every bare call site. The bar: an idiom earns its place when it makes the call clearer, not just shorter, and never at the cost of the bare-call feel or the typed-core split.

## Project-generator kinds that are gated

The platform kinds the generator names but cannot emit (an iOS app, a Vision app, a screen saver, an AR effect) are each gated on their own platform leg rather than on the generator: opening one is a new emitter plus a changed availability, and the reasons live with those items.
