Skip to content

Docs - #97

Merged
TheTechromancer merged 20 commits into
devfrom
docs
Aug 14, 2026
Merged

Docs#97
TheTechromancer merged 20 commits into
devfrom
docs

Conversation

@TheTechromancer

@TheTechromancer TheTechromancer commented Aug 10, 2026

Copy link
Copy Markdown
Member

Prep for autogenerated documentation on the darkly website

image

What this branch does

Darkly's website is a separate, private repo. This branch builds the producer half of the boundary between them: one artifact per release that describes everything Darkly registers, complete enough that the consumer needs no Darkly source and no Darkly knowledge to render it. Getting there required unifying the registries it projects from, so most of the diff is that unification rather than the export itself.

The docs artifact

.github/workflows/docs-artifact.yml packs one tarball per release tag: metadata.json, resolved icons.json, and docs/manual/ — prose, registry projection and icons under one version, so a consumer can never pair prose from one build with metadata from another. The consumer is pull-based and deliberately unnamed; this repo is public and knows nothing about who reads the artifact.

  • bin/export-docs writes the self-describing JSON. Everything requiring Darkly knowledge to compute is computed here: layered preset resolution, chord rendering for both platform conventions, unit-suffixed display strings. It needs no GPU — every registry constructor is pure, so the catalogs build from &'static registration data alone.
  • frontend/scripts/export-doc-icons.mjs resolves icon names to SVG. An icon name is not self-describing and local: icons exist nowhere but this repo, so the artifact carries resolved markup rather than asking a consumer to own an icon toolchain.
  • docs/manual/ is the hand-written half — pure markdown, no consumer components, or the public repo acquires an invisible dependency on private code.

One catalog shape for every registry

catalog.rs is the core of it. Filters, veils, voids, blend modes, tools, layer kinds and layer filters all answer the same four questions — name, appearance, behaviour, settable parameters — and each answered them through its own flat *TypeInfo struct and its own engine query. Catalog / CatalogEntry replaces all of them.

Each registration type builds its own entry in its own file, so nothing in catalog.rs branches on which registry it is looking at, and adding a veil still touches nothing outside gpu/veils/. build.rs generates the enumeration of catalog-producing registries from the module directories it already scans — a registry cannot be silently left out of the projection. Tools joined the same registry (three lines per tool file).

The UI pickers, the settings surface and the metadata export now consume one shape instead of three.

Actions and hotkeys became Rust-owned

action.rs plus actions/*.rs split an action into two halves. The documentable half — id, label, icon, one-line description — is static data living beside the presets/*.yaml bindings that name the same ids. The behavioural half closes over Svelte runes and stays in frontend/src/actions/. They join by id, and action_metadata_join.test.ts holds the two sides to it.

Categories are one file each (edit.rs, view.rs, layers.rs, …), the same many-items-per-file shape config/sections/ uses and the shape GIMP's per-domain GimpActionEntry tables use. build.rs discovers them.

config/chord.rs moves binding-grammar parsing and chord rendering into Rust. The artifact ships chords already rendered for both Mac and non-Mac conventions — a second implementation of that table on the consumer side is exactly what the artifact exists to avoid. frontend/src/config/schema.ts is deleted; the schema now comes from Rust.

Preview system unification

gpu/veil_preview.rs and gpu/void_preview.rs are gone, absorbed into gpu/preview.rs. An entry declares that it has a preview (a PreviewAnim on its registration) and how it moves as code (preview_at); one driver renders both the editor's pickers and the documentation frames, so a preview can't look one way in the app and another in the docs.

Two invariants hold it together:

  • Absolute, not incremental. preview_at(0.5) produces the same state whether it follows preview_at(0.4) or nothing at all — which is what lets a sequence be rebuilt, resumed or sampled out of order. preview_at_is_absolute in tests/picker_preview.rs enforces it across every entry.
  • docs_render/ renders nothing of its own. The motion belongs to the entry; what's left in the module is only what a headless run needs — a fixed synthetic subject instead of the user's canvas, a blocking capture sink, PNGs on disk, and an index beside them. Blend modes and brushes get the two leftover renderers, since neither is an effect over one image.

bin/render-docs is a separate binary from export-docs on purpose: folding them together would drag the GPU-free metadata export behind a GPU device and the testing feature it doesn't need. Preview encoding (poster + looping H.264) is not in this branch.

Brush engine

  • Liquify is a warp field now (brush/warp_field.rs). A warp terminal's whole stroke is expressible as one coordinate map, so rasterising the intermediate states was both wasteful and destructive — at liquify's 4 px dab spacing a pixel passes under dozens of dabs per swipe, and a chain of bilinear filters is a low-pass cascade, not a bilinear filter. The scratch holds the map; one resample at the end turns it back into pixels.
  • Author-declared port ranges. A brush author can re-range any input port per instance (PortDef::min/max), so a control whose registration range is a poor fit becomes usable without a helper node doing arithmetic in the graph. The range lives on the instance port, so the brush bar and the node editor both see it.
  • units.rs — display units for numeric values, at the crate root because node-graph ports and effect parameters both declare one and neither owns the table.
  • Brush stroke previews, preview staging for content-dependent brushes, and a deterministic preview seed.

Frontend

lib/scrubDrag.ts gives value scrubs a proper drag lifecycle: preview locally while the pointer moves, commit once on release. The intermediate values of a scrub are transient session state — sending each one to the engine made every frame of a gesture a committed mutation, recompiling the brush graph and re-deriving its previews for values the user was only passing through. It's kept free of DOM and app state so it unit-tests headlessly under vitest.

Also: wasm-watch.mjs for the dev loop, and tool glyph handling.

Spelling

Registry prose, parameter labels, descriptions and option labels went colourcolor throughout. These strings are published in the artifact, so the inconsistency was user-visible.

Tests

New: docs_export.rs, docs_render.rs, picker_preview.rs, brush_preview_staging.rs, port_ranges.rs, warp_field.rs, action_metadata_join.test.ts, scrubDrag.test.ts, toolGlyph.test.ts. veil_preview.rs and void_preview.rs were removed along with the modules they covered.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

@TheTechromancer

TheTechromancer commented Aug 14, 2026

Copy link
Copy Markdown
Member Author
2026-08-13_22-47-06.mp4
2026-08-13_23-12-47.mp4
2026-08-13_23-16-41.mp4
image image

@TheTechromancer
TheTechromancer merged commit 87a4085 into dev Aug 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant